diff options
Diffstat (limited to 'lex-serve')
-rw-r--r-- | lex-serve/assets/logo.svg | 1 | ||||
-rw-r--r-- | lex-serve/assets/script.js | 56 | ||||
-rw-r--r-- | lex-serve/assets/style.css | 40 | ||||
-rw-r--r-- | lex-serve/countries.json | 218 | ||||
-rw-r--r-- | lex-serve/main.go | 144 | ||||
-rw-r--r-- | lex-serve/main_test.go | 144 | ||||
-rw-r--r-- | lex-serve/templates/index.html.tmpl | 17 | ||||
-rw-r--r-- | lex-serve/templates/search.html.tmpl | 30 |
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 national 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 national 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> |