From 50ea018252ce69542eab6a107b99ea8179810d1e Mon Sep 17 00:00:00 2001 From: Martin Fischer Date: Fri, 11 Apr 2025 16:33:59 +0200 Subject: refactor: introduce lex-serve package --- lex-serve/assets/logo.svg | 1 + lex-serve/assets/script.js | 56 +++++++++ lex-serve/assets/style.css | 40 +++++++ lex-serve/countries.json | 218 +++++++++++++++++++++++++++++++++++ lex-serve/main.go | 144 +++++++++++++++++++++++ lex-serve/main_test.go | 144 +++++++++++++++++++++++ lex-serve/templates/index.html.tmpl | 17 +++ lex-serve/templates/search.html.tmpl | 30 +++++ 8 files changed, 650 insertions(+) create mode 100644 lex-serve/assets/logo.svg create mode 100644 lex-serve/assets/script.js create mode 100644 lex-serve/assets/style.css create mode 100644 lex-serve/countries.json create mode 100644 lex-serve/main.go create mode 100644 lex-serve/main_test.go create mode 100644 lex-serve/templates/index.html.tmpl create mode 100644 lex-serve/templates/search.html.tmpl (limited to 'lex-serve') diff --git a/lex-serve/assets/logo.svg b/lex-serve/assets/logo.svg new file mode 100644 index 0000000..45de364 --- /dev/null +++ b/lex-serve/assets/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lex-serve/assets/script.js b/lex-serve/assets/script.js new file mode 100644 index 0000000..95b8393 --- /dev/null +++ b/lex-serve/assets/script.js @@ -0,0 +1,56 @@ +const searchInput = document.getElementById('search'); +const suggestionsDiv = document.getElementById('suggestions'); + +async function enableAutocomplete() { + const res = await fetch('/laws.json'); + const laws = await res.json(); + // TODO: strip accents before searching + + searchInput.addEventListener('input', (e) => { + if (searchInput.value == '') { + suggestionsDiv.innerHTML = ''; + return; + } + + const titleRegex = new RegExp(searchInput.value, 'i'); + const abbrRegex = new RegExp('^' + searchInput.value, 'i'); + const suggestions = []; + + laws.map(l => { + const titleMatch = titleRegex.exec(l.title); + const abbrMatch = abbrRegex.exec(l.abbr); + + return { + law: l, + titleScore: titleMatch ? titleMatch.index : Number.MAX_VALUE, + abbrScore: abbrMatch ? abbrMatch.index: Number.MAX_VALUE, + } + }) + .filter(l => l.titleScore < Number.MAX_VALUE || l.abbrScore < Number.MAX_VALUE) + .sort((a,b) => { + let abbrDiff = a.abbrScore - b.abbrScore; + if (a.law.abbr && b.law.abbr && abbrDiff == 0 && a.abbrScore != Number.MAX_VALUE) { + abbrDiff = a.law.abbr.length - b.law.abbr.length; + } + return abbrDiff || a.titleScore - b.titleScore; + }) + .slice(0, 30) + .forEach(x => { + const l = x.law; + const a = document.createElement('a'); + if (l.redir) + a.href = '/' + l.redir; + else + a.href = l.url; + a.textContent = l.title; + const li = document.createElement('li'); + li.appendChild(a); + suggestions.push(li); + }); + + suggestionsDiv.replaceChildren(...suggestions); + }); +} + +if ('json' in searchInput.dataset) + enableAutocomplete(); \ No newline at end of file diff --git a/lex-serve/assets/style.css b/lex-serve/assets/style.css new file mode 100644 index 0000000..3b064c2 --- /dev/null +++ b/lex-serve/assets/style.css @@ -0,0 +1,40 @@ +body { + font-family: Roboto, Helvetica, Arial, sans-serif; + max-width: 500px; + margin: 0.5em auto; + text-align: center; +} + +.countries { + text-align: left; + border: 1px solid #ccc; + border-radius: 5px; + + display: grid; + grid-template-columns: repeat(auto-fit, minmax(50px, 1fr)); +} + +.cc-link { + display: inline-block; + line-height: 50px; + text-align: center; + color: inherit; + text-decoration: navajowhite; + font-size: 1.2em; + border: 2px solid transparent; + box-sizing: border-box; +} + +.cc-link:hover { + background: #e6f3ff; + border-radius: 5px; +} + +h2 { + font-weight: normal; + margin-top: 0.5em; +} + +#suggestions { + text-align: left; +} \ No newline at end of file diff --git a/lex-serve/countries.json b/lex-serve/countries.json new file mode 100644 index 0000000..d7f8cc1 --- /dev/null +++ b/lex-serve/countries.json @@ -0,0 +1,218 @@ +{ + "al": { + "name": "Albania", + "search_url": "https://qbz.gov.al/search;q=%s" + }, + "am": { + "name": "Armenia", + "search_url": "http://www.parliament.am/search.php?where=laws&what=%s" + }, + "ar": { + "name": "Argentina", + "search_url": "https://www.boletinoficial.gob.ar/busquedaAvanzada/primera" + }, + "at": { + "name": "Austria", + "search_url": "https://www.ris.bka.gv.at/Ergebnis.wxe?Abfrage=Gesamtabfrage&Suchworte=%s" + }, + "au": { + "name": "Australia", + "search_url": "https://www.legislation.gov.au/Search/%s" + }, + "az": { + "name": "Azerbaijan", + "search_url": "https://meclis.gov.az/axtar-qanun.php?cat=72&soz=%s" + }, + "be": { + "name": "Belgium", + "search_url": "https://www.ejustice.just.fgov.be/cgi_loi/loi_rech.pl?liste.x=&liste.y=&language=nl&text1=%s" + }, + "bg": { + "name": "Bulgaria", + "search_url": "https://dv.parliament.bg/DVWeb/broeveList.faces" + }, + "br": { + "name": "Brazil", + "search_url": "https://legislacao.presidencia.gov.br/" + }, + "by": { + "name": "Belarus", + "search_url": "https://etalonline.by/kodeksy/" + }, + "ca": { + "name": "Canada", + "search_url": "https://laws-lois.justice.gc.ca/Search/Search.aspx?txtS3archA11=%s" + }, + "ch": { + "name": "Switzerland", + "search_url": "https://www.fedlex.admin.ch/de/search?text=%s" + }, + "cy": { + "name": "Cyprus", + "search_url": "http://www.cylaw.org/cgi-bin/sinocgi.pl?directories=9&query=%s" + }, + "cz": { + "name": "Czech Republic", + "search_url": "https://aplikace.mvcr.cz/sbirka-zakonu/SearchResult.aspx?typeLaw=zakon&what=Text_v_anotaci&q=%s" + }, + "de": { + "name": "Germany", + "search_url": "https://www.gesetze-im-internet.de/cgi-bin/htsearch?config=Titel_bmjhome2005&method=and&words=%s" + }, + "dk": { + "name": "Denmark", + "search_url": "https://www.retsinformation.dk/documents?t=%s" + }, + "ee": { + "name": "Estonia", + "search_url": "https://www.riigiteataja.ee/otsingu_tulemus.html?otsisona=%s" + }, + "es": { + "name": "Spain", + "search_url": "https://www.boe.es/buscar/legislacion.php?campo%5B0%5D=ID_SRC&dato%5B0%5D=&operador%5B0%5D=and&campo%5B1%5D=NOVIGENTE&operador%5B1%5D=and&campo%5B3%5D=CONSO&operador%5B3%5D=and&campo%5B2%5D=TIT&dato%5B2%5D=%s&checkbox_solo_tit=S&operador%5B2%5D=and&page_hits=50&sort_field%5B0%5D=PESO&sort_order%5B0%5D=desc&sort_field%5B1%5D=ref&sort_order%5B1%5D=asc&accion=Buscar" + }, + "fi": { + "name": "Finland", + "search_url": "https://finlex.fi/fi/laki/haku/?search[type]=pika&search[pika]=%s" + }, + "fr": { + "name": "France", + "search_url": "https://www.legifrance.gouv.fr/search/all?tab_selection=all&searchField=ALL&query=%s" + }, + "gr": { + "name": "Greece", + "search_url": "http://www.et.gr/index.php/nomoi-proedrika-diatagmata" + }, + "hr": { + "name": "Croatia", + "search_url": "https://sredisnjikatalogrh.gov.hr/cadial/search.php?action=search&query=%s" + }, + "hu": { + "name": "Hungary", + "search_url": "https://njt.hu/" + }, + "ie": { + "name": "Ireland", + "search_url": "http://www.irishstatutebook.ie/eli/ResultsTitle.html?q=%s" + }, + "in": { + "name": "India", + "search_url": "https://www.indiacode.nic.in/handle/123456789/1362/simple-search?query=%s" + }, + "is": { + "name": "Iceland", + "search_url": "https://www.stjornartidindi.is/" + }, + "it": { + "name": "Italy", + "search_url": "https://www.normattiva.it/ricerca/veloce/0?testoRicerca=%s" + }, + "jp": { + "name": "Japan", + "search_url": "https://elaws.e-gov.go.jp/result?allDocument=0&searchTargetAll=1&searchTextBox=%s&serchSelect=and&searchTarget_array=Constitution%2CAct%2CCabinetOrder%2CImperialOrder%2CMinisterialOrdinance%2CRule&classification_array=%E6%86%B2%E6%B3%95%2C%E5%88%91%E4%BA%8B%2C%E8%B2%A1%E5%8B%99%E9%80%9A%E5%89%87%2C%E6%B0%B4%E7%94%A3%E6%A5%AD%2C%E8%A6%B3%E5%85%89%2C%E5%9B%BD%E4%BC%9A%2C%E8%AD%A6%E5%AF%9F%2C%E5%9B%BD%E6%9C%89%E8%B2%A1%E7%94%A3%2C%E9%89%B1%E6%A5%AD%2C%E9%83%B5%E5%8B%99%2C%E8%A1%8C%E6%94%BF%E7%B5%84%E7%B9%94%2C%E6%B6%88%E9%98%B2%2C%E5%9B%BD%E7%A8%8E%2C%E5%B7%A5%E6%A5%AD%2C%E9%9B%BB%E6%B0%97%E9%80%9A%E4%BF%A1%2C%E5%9B%BD%E5%AE%B6%E5%85%AC%E5%8B%99%E5%93%A1%2C%E5%9B%BD%E5%9C%9F%E9%96%8B%E7%99%BA%2C%E4%BA%8B%E6%A5%AD%2C%E5%95%86%E6%A5%AD%2C%E5%8A%B4%E5%83%8D%2C%E8%A1%8C%E6%94%BF%E6%89%8B%E7%B6%9A%2C%E5%9C%9F%E5%9C%B0%2C%E5%9B%BD%E5%82%B5%2C%E9%87%91%E8%9E%8D%E3%83%BB%E4%BF%9D%E9%99%BA%2C%E7%92%B0%E5%A2%83%E4%BF%9D%E5%85%A8%2C%E7%B5%B1%E8%A8%88%2C%E9%83%BD%E5%B8%82%E8%A8%88%E7%94%BB%2C%E6%95%99%E8%82%B2%2C%E5%A4%96%E5%9B%BD%E7%82%BA%E6%9B%BF%E3%83%BB%E8%B2%BF%E6%98%93%2C%E5%8E%9A%E7%94%9F%2C%E5%9C%B0%E6%96%B9%E8%87%AA%E6%B2%BB%2C%E9%81%93%E8%B7%AF%2C%E6%96%87%E5%8C%96%2C%E9%99%B8%E9%81%8B%2C%E7%A4%BE%E4%BC%9A%E7%A6%8F%E7%A5%89%2C%E5%9C%B0%E6%96%B9%E8%B2%A1%E6%94%BF%2C%E6%B2%B3%E5%B7%9D%2C%E7%94%A3%E6%A5%AD%E9%80%9A%E5%89%87%2C%E6%B5%B7%E9%81%8B%2C%E7%A4%BE%E4%BC%9A%E4%BF%9D%E9%99%BA%2C%E5%8F%B8%E6%B3%95%2C%E7%81%BD%E5%AE%B3%E5%AF%BE%E7%AD%96%2C%E8%BE%B2%E6%A5%AD%2C%E8%88%AA%E7%A9%BA%2C%E9%98%B2%E8%A1%9B%2C%E6%B0%91%E4%BA%8B%2C%E5%BB%BA%E7%AF%89%E3%83%BB%E4%BD%8F%E5%AE%85%2C%E6%9E%97%E6%A5%AD%2C%E8%B2%A8%E7%89%A9%E9%81%8B%E9%80%81%2C%E5%A4%96%E4%BA%8B&lawNo1=&lawNo2=&lawNo3=&lawNo4=&dayPromulgation1_0=&dayPromulgation1_1=&dayPromulgation1_2=&dayPromulgation1_3=&dayPromulgation2_0=&dayPromulgation2_1=&dayPromulgation2_2=&dayPromulgation2_3=&dayPromulgation1=&dayPromulgation2=&lawDayFrom1=0&selectMenu=0&searchBtn=1" + }, + "lt": { + "name": "Lithuania", + "search_url": "https://e-seimas.lrs.lt/portal/simpleSearch/lt" + }, + "lu": { + "name": "Luxembourg", + "search_url": "http://legilux.public.lu/search/?fulltext=%s" + }, + "lv": { + "name": "Latvia", + "search_url": "https://www.vestnesis.lv/rezultati/atbilstiba/on/locijums/on/lapa/1/skaits/20/kartot/datums/ta/on/izdeveji/saeima.likumi/teksts/%s" + }, + "ma": { + "name": "Malta", + "search_url": "https://legislation.mt/Legislation" + }, + "mk": { + "name": "Macedonia", + "search_url": "https://vlada.mk/search/node/%s%20language%3Amk%2Cund" + }, + "mx": { + "name": "Mexico", + "search_url": "http://www.diputados.gob.mx/LeyesBiblio/index.htm" + }, + "nl": { + "name": "Netherlands", + "search_url": "https://wetten.overheid.nl/zoeken/zoekresultaat/rs/2,3,4/titel/%s/titelf/1/tekstf/1/artnrb/0/d//dx/0" + }, + "no": { + "name": "Norway", + "search_url": "https://lovdata.no/sok?q=%s" + }, + "nz": { + "name": "New Zealand", + "search_url": "https://www.legislation.govt.nz/all/results.aspx?search=ts_act%40bill%40regulation%40deemedreg_%s_resel_25_a" + }, + "pe": { + "name": "Peru", + "search_url": "https://leyes.congreso.gob.pe/LeyNume_1p.aspx?xEstado=2&xTipoBusqueda=3&xTexto=%s" + }, + "ph": { + "name": "Philippines", + "search_url": "https://www.officialgazette.gov.ph/the-philippine-legal-codes/" + }, + "pk": { + "name": "Pakistan", + "search_url": "http://www.punjablaws.gov.pk/index6.html" + }, + "pl": { + "name": "Poland", + "search_url": "http://isap.sejm.gov.pl/isap.nsf/search.xsp?status=O&title=%s" + }, + "pt": { + "name": "Portugal", + "search_url": "https://dre.pt/web/guest/pesquisa/-/search/basic?q=%s" + }, + "py": { + "name": "Paraguay", + "search_url": "http://digesto.senado.gov.py/buscar/buscar?buscar=%s" + }, + "ro": { + "name": "Romania", + "search_url": "http://legislatie.just.ro/Public/RezultateCautare?titlu=%s" + }, + "rs": { + "name": "Serbia", + "search_url": "https://www.pravno-informacioni-sistem.rs/reg-search?q=%s" + }, + "ru": { + "name": "Russia", + "search_url": "http://pravo.gov.ru/proxy/ips/?searchres=&bpas=cd00000&sort=-1&intelsearch=%s" + }, + "se": { + "name": "Sweden", + "search_url": "https://www.riksdagen.se/sv/global/sok/?doktyp=sfs&q=%s" + }, + "si": { + "name": "Slovenia", + "search_url": "http://www.pisrs.si/Pis.web/indexSearch?search=%s" + }, + "sk": { + "name": "Slovakia", + "search_url": "https://www.slov-lex.sk/vyhladavanie-pravnych-predpisov?text=%s" + }, + "tk": { + "name": "Turkey", + "search_url": "https://www.mevzuat.gov.tr/aramasonuc?AranacakMetin=%s" + }, + "ua": { + "name": "Ukraine", + "search_url": "https://zakon.rada.gov.ua/laws/main?find=2&dat=00000000&lang=en&user=a&text=%s&textl=2&bool=and&org=0&typ=1&datl=0&yer=0000&mon=00&day=00&numl=2&num=&minjustl=2&minjust=" + }, + "uk": { + "name": "the United Kingdom", + "search_url": "https://www.legislation.gov.uk/primary+secondary?title=%s" + }, + "us": { + "name": "the United States", + "search_url": "https://www.loc.gov/search/?fa=original-format:legislation&q=%s" + }, + "uy": { + "name": "Uruguay", + "search_url": "https://parlamento.gub.uy/documentosyleyes/leyes?Searchtext=%s" + } +} diff --git a/lex-serve/main.go b/lex-serve/main.go new file mode 100644 index 0000000..d0fb690 --- /dev/null +++ b/lex-serve/main.go @@ -0,0 +1,144 @@ +package main + +import ( + "embed" + "encoding/json" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "strings" + "text/template" +) + +//go:embed countries.json +var countriesJSON []byte + +func main() { + domain := os.Getenv("DOMAIN") + if domain == "" { + log.Fatal("DOMAIN environment variable must be set") + } + + var handler = handler{domain: domain, lawsByCC: map[string]map[string]law{}} + err := json.Unmarshal(countriesJSON, &handler.countries) + if err != nil { + log.Fatal("countries.json ", err) + } + + lawFiles, err := ioutil.ReadDir("laws") + if err != nil { + log.Fatal(err) + } + for _, file := range lawFiles { + text, err := ioutil.ReadFile("laws/" + file.Name()) + if err != nil { + log.Fatal(file.Name(), err) + } + var laws []law + err = json.Unmarshal([]byte(text), &laws) + if err != nil { + log.Fatal(file.Name(), err) + } + cc := strings.SplitN(file.Name(), ".", 2)[0] + handler.lawsByCC[cc] = map[string]law{} + for _, law := range laws { + if law.Redir != "" { + handler.lawsByCC[cc][law.Redir] = law + } + } + } + http.HandleFunc("/", handler.handle) + println("listening on 8000") + log.Fatal(http.ListenAndServe(":8000", nil)) +} + +//go:embed templates +var templates embed.FS + +var tpl, _ = template.New("").Funcs(template.FuncMap{ + "ToUpper": strings.ToUpper, +}).ParseFS(templates, "templates/*") + +type handler struct { + domain string + countries map[string]country + lawsByCC map[string]map[string]law +} + +func (h *handler) handle(w http.ResponseWriter, r *http.Request) { + if r.Host == h.domain { + if r.URL.Path != "/" { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("page not found")) + return + } + err := tpl.ExecuteTemplate(w, "index.html.tmpl", map[string]any{ + "Countries": h.countries, + "Domain": r.Host, + }) + if err != nil { + log.Fatal(err) + } + return + } + + cc, isSubdomain := strings.CutSuffix(r.Host, "."+h.domain) + if !isSubdomain { + w.Write([]byte("unknown host")) + return + } + key := strings.TrimLeft(r.URL.Path, "/") + if len(key) > 0 { + val, ok := h.lawsByCC[cc][key] + if !ok { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("unknown law")) + return + } + http.Redirect(w, r, val.URL, 302) + } else { + query := r.URL.Query().Get("q") + if query != "" { + country, ok := h.countries[cc] + if !ok { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("search not implemented for this country")) + return + } + if country.HasPlaceholder() { + http.Redirect(w, r, strings.Replace(country.SearchURL, "%s", url.QueryEscape(query), 1), 302) + return + } else { + w.WriteHeader(http.StatusBadRequest) + } + } + _, hasJSONLaws := h.lawsByCC[cc] + err := tpl.ExecuteTemplate(w, "search.html.tmpl", map[string]any{ + "TLD": cc, + "Domain": h.domain, + "Country": h.countries[cc], + "HasJSONLaws": hasJSONLaws, + }) + if err != nil { + log.Fatal(err) + } + } +} + +type law struct { + URL string + Title string + Abbr string + Redir string +} + +type country struct { + Name string + SearchURL string `json:"search_url"` +} + +func (c country) HasPlaceholder() bool { + return strings.Contains(c.SearchURL, "%s") +} diff --git a/lex-serve/main_test.go b/lex-serve/main_test.go new file mode 100644 index 0000000..e647a2f --- /dev/null +++ b/lex-serve/main_test.go @@ -0,0 +1,144 @@ +package main + +import ( + "net/http/httptest" + "strings" + "testing" + + "github.com/peter-evans/patience" +) + +func newHandler() handler { + return handler{ + domain: "lex.example", + countries: map[string]country{ + "zu": country{ + Name: "Zubrowka", + SearchURL: "https://lex.gov.zu/?search=%s", + }, + }, + lawsByCC: map[string]map[string]law{}, + } +} + +func TestStartPage(t *testing.T) { + h := newHandler() + + req := httptest.NewRequest("GET", "/", nil) + req.Host = "lex.example" + w := httptest.NewRecorder() + h.handle(w, req) + + resp := w.Result() + if resp.StatusCode != 200 { + t.Errorf("expected status 200 OK, got %d", resp.StatusCode) + } + expected := ` + + + Lex.surf: Portal to National Law + + + + + +

The portal to national law.

+
+ ZU +
+ + +` + assertEqual(t, w.Body.String(), expected) +} + +func TestSearch(t *testing.T) { + h := newHandler() + + req := httptest.NewRequest("GET", "/", nil) + req.Host = "zu.lex.example" + w := httptest.NewRecorder() + h.handle(w, req) + + resp := w.Result() + if resp.StatusCode != 200 { + t.Errorf("expected status 200 OK, got %d", resp.StatusCode) + } + expected := ` + + + National Law of Zubrowka + + + + + +

National Law of Zubrowka

+ + +
+ + + + + + +
+ + + + +` + assertEqual(t, w.Body.String(), expected) +} + +func TestSearchRedirect(t *testing.T) { + h := newHandler() + + req := httptest.NewRequest("GET", "/?q=tourism", nil) + req.Host = "zu.lex.example" + w := httptest.NewRecorder() + h.handle(w, req) + + resp := w.Result() + if resp.StatusCode != 302 { + t.Errorf("expected status 302, got %d", resp.StatusCode) + } + + if resp.Header.Get("Location") != "https://lex.gov.zu/?search=tourism" { + t.Errorf("wrong location, got %s", resp.Header.Get("Location")) + } +} + +func TestLawRedirect(t *testing.T) { + h := newHandler() + + h.lawsByCC["zu"] = map[string]law{} + h.lawsByCC["zu"]["zepl"] = law{ + URL: "https://lex.gov.zu/zeppelin-code", + } + + req := httptest.NewRequest("GET", "/zepl", nil) + req.Host = "zu.lex.example" + w := httptest.NewRecorder() + h.handle(w, req) + + resp := w.Result() + if resp.StatusCode != 302 { + t.Errorf("expected status 302, got %d", resp.StatusCode) + } + + if resp.Header.Get("Location") != "https://lex.gov.zu/zeppelin-code" { + t.Errorf("wrong location, got %s", resp.Header.Get("Location")) + } +} + +func assertEqual(t *testing.T, received string, expected string) { + if received != expected { + a := strings.Split(received, "\n") + b := strings.Split(expected, "\n") + diffs := patience.Diff(a, b) + unidiff := patience.UnifiedDiffText(diffs) + t.Error(unidiff) + } +} diff --git a/lex-serve/templates/index.html.tmpl b/lex-serve/templates/index.html.tmpl new file mode 100644 index 0000000..3d8c04f --- /dev/null +++ b/lex-serve/templates/index.html.tmpl @@ -0,0 +1,17 @@ + + + + Lex.surf: Portal to National Law + + + + + +

The portal to national law.

+
+ {{range $key, $c := .Countries -}} + {{$key | ToUpper}} + {{- end}} +
+ + diff --git a/lex-serve/templates/search.html.tmpl b/lex-serve/templates/search.html.tmpl new file mode 100644 index 0000000..70c8c86 --- /dev/null +++ b/lex-serve/templates/search.html.tmpl @@ -0,0 +1,30 @@ + + + + National Law of {{.Country.Name}} + + + + + +

National Law of {{.Country.Name}}

+ {{if not .Country.HasPlaceholder}} +

{{.Country.SearchURL}}

+ + {{if not .HasJSONLaws}} + (No search form here because the search isn't linkable.) + {{end}} + {{end}} + {{if .Country.HasPlaceholder}} +
+ {{end}} + {{if or .Country.HasPlaceholder .HasJSONLaws}} + + + {{end}} + {{if .Country.HasPlaceholder}} +
+ {{end}} + + + -- cgit v1.2.3