summaryrefslogtreecommitdiff
path: root/lex-serve
diff options
context:
space:
mode:
Diffstat (limited to 'lex-serve')
-rw-r--r--lex-serve/assets/logo.svg1
-rw-r--r--lex-serve/assets/script.js56
-rw-r--r--lex-serve/assets/style.css40
-rw-r--r--lex-serve/countries.json218
-rw-r--r--lex-serve/main.go144
-rw-r--r--lex-serve/main_test.go144
-rw-r--r--lex-serve/templates/index.html.tmpl17
-rw-r--r--lex-serve/templates/search.html.tmpl30
8 files changed, 650 insertions, 0 deletions
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 @@
+<svg height="360.71" width="242.76" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="b" gradientTransform="matrix(-1 0 0 -.7998 213.15 303.4)" gradientUnits="userSpaceOnUse" x1="171.09" x2="20.17" xlink:href="#a" y1="112.23" y2="112.23"/><linearGradient id="a"><stop offset="0" stop-color="#32a8ff"/><stop offset="1" stop-color="#3d5cff"/></linearGradient><linearGradient id="c" gradientTransform="matrix(1 0 0 .7998 20.85 0)" gradientUnits="userSpaceOnUse" x1="51.7" x2="17" xlink:href="#a" y1="80" y2="201.2"/><path d="M124.85 303.4c41.3-.4 68.5-20.95 68.5-52.95 0-19.2-6.4-28.39-32.6-47.5l-33.7-22.4c-2.7-1.6-5.5-3.36-8.7-5.6l-6.9-4.8-16.5-11.35c-14.3-9.92-19.8-16.88-19.8-24.24.44-11.07 11.96-19.78 25.8-19.51 10.5.55 20.52 3.68 28.7 8.96v-.08l-38-22.88h-2.7c-26.7 0-48.3 20.64-48.3 46.39 0 11.76 3.2 20.95 11 30.87 8.76 11.33 19.8 21.41 32.7 29.83l49.8 26.64c17.9 11.2 27 21.91 24.2 31.35-3 10.88-18 19.5-38.2 20.39-20.2.8-44.7-8.16-44.7-8.16-8.2 20.8 41.8 25.04 49.4 25.04z" fill="url(#b)"/><path d="M109.15 0c-41.3.4-68.5 20.95-68.5 52.95 0 19.2 6.4 28.39 32.6 47.5l33.7 22.4c2.7 1.6 5.5 3.36 8.7 5.6l6.9 4.8 16.5 11.35c14.3 9.92 19.8 16.88 19.8 24.24-.43 11.07-11.95 19.78-25.8 19.51-10.49-.55-20.51-3.67-28.7-8.96v.08l38 22.88h2.7c26.7 0 48.3-20.64 48.3-46.39 0-11.76-3.2-20.95-11-30.87-8.75-11.33-19.8-21.41-32.7-29.83l-49.8-26.64c-17.9-11.2-27-21.91-24.2-31.35 3-10.88 18-19.5 38.2-20.39 20.2-.8 44.7 8.16 44.7 8.16C166.75 4.24 116.75 0 109.15 0z" fill="url(#c)"/><ellipse cx="207.61" cy="78.82" fill="#47e0e1" rx="79" ry="17.8" transform="rotate(37.5)"/><g fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round"><path d="M103.55 171.65l1-30.4 57.7-7.9-25.2 24 3.8 44.7" stroke-width="15.5"/><path d="M162.25 133.35l-21-45.1-37.5 11.8m37.5-11.8l41 17.3" stroke-width="15"/></g><circle cx="141.55" cy="60.55" r="15.7"/><g stroke-width="121.41"><path d="M0 360.03v-50.4h9.54v50.4zM34.34 360.7q-8.29 0-12.74-4.88-4.45-4.93-4.45-14.34 0-9.1 4.52-14 4.52-4.88 12.8-4.88 7.92 0 12.1 5.26 4.18 5.23 4.18 15.36v.27H27.17q0 5.36 1.97 8.12 2 2.71 5.68 2.71 5.06 0 6.38-4.38l9 .78q-3.9 9.99-15.86 9.99zm0-32.09q-3.36 0-5.2 2.34-1.8 2.35-1.9 6.56h14.27q-.27-4.45-2.14-6.66-1.87-2.24-5.03-2.24zM80.98 360.03l-8.56-13.32-8.63 13.32H53.63l13.46-18.99-12.81-17.76h10.3l7.84 12.02 7.81-12.02h10.36l-12.8 17.66 13.55 19.09zM96.6 360.03v-10.36h9.82v10.36zM147.01 349.3q0 5.33-4.38 8.39-4.35 3.02-12.06 3.02-7.57 0-11.62-2.38-4-2.41-5.33-7.47l8.39-1.26q.71 2.62 2.45 3.7 1.76 1.1 6.11 1.1 4 0 5.84-1.03 1.84-1.02 1.84-3.2 0-1.76-1.5-2.78-1.46-1.05-5-1.76-8.08-1.6-10.9-2.96-2.81-1.39-4.3-3.56-1.47-2.21-1.47-5.4 0-5.27 4.04-8.2 4.08-2.95 11.52-2.95 6.55 0 10.53 2.55 4 2.55 5 7.37l-8.47.89q-.4-2.25-2-3.33-1.6-1.12-5.06-1.12-3.4 0-5.1.88-1.7.85-1.7 2.89 0 1.6 1.3 2.54 1.32.92 4.41 1.53 4.31.89 7.64 1.84 3.37.91 5.37 2.2 2.04 1.3 3.23 3.33 1.22 2 1.22 5.17zM163.76 323.28v20.62q0 9.68 6.52 9.68 3.46 0 5.57-2.96 2.14-2.99 2.14-7.64v-19.7h9.54v28.53q0 4.69.27 8.22h-9.1q-.4-4.9-.4-7.3h-.18q-1.9 4.17-4.85 6.08-2.92 1.9-6.97 1.9-5.84 0-8.96-3.57-3.13-3.6-3.13-10.53v-23.33zM197.28 360.03v-28.12q0-3.03-.1-5.03-.07-2.04-.17-3.6h9.1q.1.6.27 3.73.17 3.1.17 4.11h.14q1.4-3.87 2.48-5.43 1.09-1.6 2.58-2.34 1.5-.79 3.74-.79 1.83 0 2.95.51v7.99q-2.3-.51-4.07-.51-3.57 0-5.57 2.88-1.97 2.9-1.97 8.56v18.04zM235.53 329.73v30.3h-9.51v-30.3h-5.37v-6.45h5.37v-3.84q0-5 2.65-7.4 2.65-2.42 8.05-2.42 2.68 0 6.04.55v6.14q-1.39-.3-2.78-.3-2.45 0-3.47.98-.98.95-.98 3.4v2.89h7.23v6.45z"/></g></svg> \ 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 := `<!doctype html>
+<html>
+<head>
+ <title>Lex.surf: Portal to National Law</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
+ <link rel="stylesheet" href="/assets/style.css" />
+</head>
+<body>
+ <img alt="lex.surf" height=140 class=logo src="/assets/logo.svg">
+ <h2>The portal to&nbsp;national&nbsp;law.</h2>
+ <div class=countries>
+ <a class=cc-link href="//zu.lex.example" title="Zubrowka">ZU</a>
+ </div>
+</body>
+</html>
+`
+ 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 := `<!doctype html>
+<html>
+<head>
+ <title>National Law of Zubrowka</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
+ <link rel="stylesheet" href="//lex.example/assets/style.css" />
+</head>
+<body>
+ <a href="//lex.example"><img alt="lex.surf" height=140 class=logo src="//lex.example/assets/logo.svg"></a>
+ <h1>National Law of Zubrowka</h1>
+
+
+ <form>
+
+
+ <input id=search name=q aria-label="Search" autocomplete="off" autofocus >
+ <ul id=suggestions></ul>
+
+
+ </form>
+
+ <script src="//lex.example/assets/script.js"></script>
+</body>
+</html>
+`
+ 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 @@
+<!doctype html>
+<html>
+<head>
+ <title>Lex.surf: Portal to National Law</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
+ <link rel="stylesheet" href="/assets/style.css" />
+</head>
+<body>
+ <img alt="lex.surf" height=140 class=logo src="/assets/logo.svg">
+ <h2>The portal to&nbsp;national&nbsp;law.</h2>
+ <div class=countries>
+ {{range $key, $c := .Countries -}}
+ <a class=cc-link href="//{{$key}}.{{$.Domain}}" title="{{$c.Name}}">{{$key | ToUpper}}</a>
+ {{- end}}
+ </div>
+</body>
+</html>
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 @@
+<!doctype html>
+<html>
+<head>
+ <title>National Law of {{.Country.Name}}</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
+ <link rel="stylesheet" href="//{{.Domain}}/assets/style.css" />
+</head>
+<body>
+ <a href="//{{.Domain}}"><img alt="lex.surf" height=140 class=logo src="//{{.Domain}}/assets/logo.svg"></a>
+ <h1>National Law of {{.Country.Name}}</h1>
+ {{if not .Country.HasPlaceholder}}
+ <p><big><a href="{{.Country.SearchURL}}">{{.Country.SearchURL}}</a></big></p>
+
+ {{if not .HasJSONLaws}}
+ (No search form here because the search isn't linkable.)
+ {{end}}
+ {{end}}
+ {{if .Country.HasPlaceholder}}
+ <form>
+ {{end}}
+ {{if or .Country.HasPlaceholder .HasJSONLaws}}
+ <input id=search name=q aria-label="Search" autocomplete="off" autofocus {{if .HasJSONLaws}}data-json{{end}}>
+ <ul id=suggestions></ul>
+ {{end}}
+ {{if .Country.HasPlaceholder}}
+ </form>
+ {{end}}
+ <script src="//{{.Domain}}/assets/script.js"></script>
+</body>
+</html>