Compare commits

...

6 Commits

Author SHA1 Message Date
c07b8331c3 fix: logic to construct SVG (#2)
Fixes #1

<img width="682" alt="image.png" src="attachments/6109d59a-ba6c-4614-a248-5abf5a4c2122">

Reviewed-on: #2
Co-authored-by: Aditya <adithya18062000@gmail.com>
Co-committed-by: Aditya <adithya18062000@gmail.com>
2025-12-13 04:46:05 +00:00
Kailash Nadh
0532643cd8 Add og-image thumb to template. 2025-11-30 23:18:26 +05:30
Kailash Nadh
846d9329cd Change template URLs to relative. 2025-11-30 22:54:08 +05:30
Kailash Nadh
ee156dd11b Fix language strings. 2025-11-30 22:49:24 +05:30
Kailash Nadh
5cfb2d8e6d Fix autocomp behaviour to ignore non-Kannada inputs and always select the first suggestion. 2025-11-30 20:58:27 +05:30
Kailash Nadh
acb421a26b Tweak autocomp style and make auto-select behaviour configurable from the template. 2025-11-30 20:45:15 +05:30
7 changed files with 132 additions and 27 deletions

View File

@@ -5,23 +5,24 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
{{- block "meta" . -}} {{ block "meta" . -}}
<title> <title>
{{- if eq .Data.PageType "/" }} {{- .L.T "global.siteName" -}} {{- if eq .Data.PageType "/" }} {{- .L.T "global.siteName" -}}
{{- else if eq .Data.PageType "glossary" }}{{- .L.T "public.glossaryTitle" -}} {{- else if eq .Data.PageType "glossary" }}{{- .L.Ts "public.glossary" "lang" (title .Data.Glossary.FromLang) -}}
{{- else if eq .Data.PageType "search" }}{{- .L.Ts "public.searchTitle" "query" .Data.Query.Query -}} {{- else if eq .Data.PageType "search" }}{{- .L.Ts "public.searchTitle" "query" .Data.Query.Query "fromLang" .Data.Query.ToLang -}}
{{- else if ne .Data.Title "" }}{{ .Data.Title }} {{- else if ne .Data.Title "" }}{{ .Data.Title }}
{{- end -}} {{- end -}}
</title> </title>
<meta name="description" value=" <meta name="description" value="
{{- if eq .Data.PageType "/" }}Dictionary website {{- if eq .Data.PageType "/" }}{{- .L.T "global.siteName" -}}
{{- else if eq .Data.PageType "glossary" }}Glossary of words. {{- else if eq .Data.PageType "glossary" }}{{- .L.Ts "public.glossary" "lang" (title .Data.Glossary.FromLang) -}}
{{- else if eq .Data.PageType "search" }}{{ .Data.Query.Query }} meaning. {{- else if eq .Data.PageType "search" }}{{- .L.Ts "public.searchDescription" "query" .Data.Query.Query "fromLang" .Data.Query.FromLang "toLang" .Data.Query.ToLang -}}
{{- else if ne .Data.Description "" }}{{ .Data.Description }} {{- else if ne .Data.Description "" }}{{ .Data.Description }}
{{- else }}{{ block "description" . }}{{end}} {{- else }}{{ block "description" . }}{{end}}
{{- end -}}" /> {{- end -}}" />
{{- end -}} {{- end }}
<meta property="og:image" content="{{ .Consts.RootURL }}/static/thumb.png">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<script> <script>
window._ROOT_URL = ""; window._ROOT_URL = "";
@@ -37,7 +38,7 @@
<header class="header"> <header class="header">
<div class="row"> <div class="row">
<div class="logo four columns"> <div class="logo four columns">
<a href="/"><img src="/static/logo.svg?v={{ .AssetVer }}" alt="Dictionary logo" /></a> <a href="/"><img src="/static/logo.svg?v={{ .AssetVer }}" alt="Alar" /></a>
<h3 class="intro"> <h3 class="intro">
<p>ಶ್ರೀ. ವಿ. ಕೃಷ್ಣ ಅವರ ಕನ್ನಡ - ಇಂಗ್ಲಿಷ್ ನಿಘಂಟು</p> <p>ಶ್ರೀ. ವಿ. ಕೃಷ್ಣ ಅವರ ಕನ್ನಡ - ಇಂಗ್ಲಿಷ್ ನಿಘಂಟು</p>
“Alar” V. Krishna's Kannada → English dictionary “Alar” V. Krishna's Kannada → English dictionary
@@ -58,7 +59,7 @@
</select> </select>
<div class="input-group"> <div class="input-group">
<input autofocus autocomplete="off" required placeholder="" aria-label="Search word" <input autofocus autocomplete="off" required placeholder="" aria-label="Search word"
type="text" id="q" name="q" value="{{ if .Data.Query }}{{ .Data.Query.Query }}{{ end }}" /> type="text" id="q" name="q" value="{{ if .Data.Query }}{{ .Data.Query.Query }}{{ end }}" data-autocomp-autoselect="false" />
<button type="submit" aria-label="Search"><img src="/static/search.svg?v={{ .AssetVer }}" alt="{{- .L.T "global.btnSearch" -}}" /></button> <button type="submit" aria-label="Search"><img src="/static/search.svg?v={{ .AssetVer }}" alt="{{- .L.T "global.btnSearch" -}}" /></button>
</div> </div>
</div> </div>
@@ -71,14 +72,14 @@
{{ define "footer" }} {{ define "footer" }}
<nav class="nav"> <nav class="nav">
{{ if .Consts.EnableSubmissions }} {{ if .Consts.EnableSubmissions }}
<a href="{{ $.Consts.RootURL }}/submit">{{- .L.T "public.submitEntry" -}}</a> <a href="/submit">{{- .L.T "public.submitEntry" -}}</a>
{{ end }} {{ end }}
{{ if .Consts.EnableGlossary }} {{ if .Consts.EnableGlossary }}
{{ range $d := .Dicts }} {{ range $d := .Dicts }}
{{ $from := index $d 0}} {{ $from := index $d 0}}
{{ $to := index $d 1}} {{ $to := index $d 1}}
<a href="{{ $.Consts.RootURL }}/glossary/{{ $from.ID }}/{{ $to.ID }}/*" class="tab"> <a href="/glossary/{{ $from.ID }}/{{ $to.ID }}/*" class="tab">
{{ title ($.L.Ts "public.glossary" "lang" "") }} {{ $.L.Ts "public.glossary" "lang" $from.Name }}
</a> </a>
{{ end }} {{ end }}
{{ end }} {{ end }}

View File

@@ -6,7 +6,7 @@
{{ else }} {{ else }}
<ul class="noul words"> <ul class="noul words">
{{ range $i, $w := $g.Words }} {{ range $i, $w := $g.Words }}
<li><a href="{{ $.Consts.RootURL }}/dictionary/{{ UnicodeURL $g.FromLang }}/{{ UnicodeURL $g.ToLang }}/{{ UnicodeURL (index $w.Content 0) }}">{{ $w.Content | join ", " }}</a></li> <li><a href="/dictionary/{{ UnicodeURL $g.FromLang }}/{{ UnicodeURL $g.ToLang }}/{{ UnicodeURL (index $w.Content 0) }}">{{ $w.Content | join ", " }}</a></li>
{{ end }} {{ end }}
</ul> </ul>
{{ end }} {{ end }}

View File

@@ -4,7 +4,7 @@
"global.btnClose": "Close", "global.btnClose": "Close",
"global.btnDelete": "Delete", "global.btnDelete": "Delete",
"global.btnSearch": "Search", "global.btnSearch": "Search",
"global.siteName": "Dictionary", "global.siteName": "Alar (ಅಲರ್) - Kannada - English dictionary. ಕನ್ನಡ-ಇಂಗ್ಲಿಷ್ ನಿಘಂಟು.",
"public.errorMessage": "An error occurred. Please try later.", "public.errorMessage": "An error occurred. Please try later.",
"public.errorTitle": "Error", "public.errorTitle": "Error",
"public.about": "About", "public.about": "About",
@@ -13,7 +13,8 @@
"public.mainTitle": "Dictionary website", "public.mainTitle": "Dictionary website",
"public.noResults": "No results where found for the query.", "public.noResults": "No results where found for the query.",
"public.noResultsTitle": "No results", "public.noResultsTitle": "No results",
"public.searchTitle": "{query} English meaning - Alar", "public.searchTitle": "{query} {fromLang} meaning - Alar",
"public.searchDescription": "{query} {fromLang} word meanings and definitions in {toLang}",
"public.similarTitle": "Similar words", "public.similarTitle": "Similar words",
"public.playAudio": "Play audio", "public.playAudio": "Play audio",
"public.subTitle": "Kannada-English dictionary", "public.subTitle": "Kannada-English dictionary",

View File

@@ -122,7 +122,7 @@ function autocomp(el, options = {}) {
return; return;
} }
val = opt.onSelect(items[idx]); val = opt.onSelect(items[idx], items);
el.value = val || items[idx]; el.value = val || items[idx];
} }

View File

@@ -273,7 +273,7 @@ async function screenshotElement(element) {
let debounce; let debounce;
autocomp(elQ, { autocomp(elQ, {
autoSelect: false, autoSelect: elQ.dataset.autocompAutoselect === "true",
onQuery: async (val) => { onQuery: async (val) => {
const langCode = localStorage.from_lang; const langCode = localStorage.from_lang;
@@ -298,8 +298,12 @@ async function screenshotElement(element) {
}); });
}, },
onSelect: (val) => { onSelect: (val, items) => {
// autocomp search isn't complete. Use the user's input instead of autocomp selection. // If the val is English, then pick the first item from items and use that.
if (/^[A-Za-z0-9\-,'" ]+$/.test(val) && items.length > 0) {
val = items[0];
}
if (val) { if (val) {
elQ.value = val; elQ.value = val;
} }

View File

@@ -25,11 +25,110 @@ async function screenshotDOM(el, opts = {}) {
if (background !== 'transparent') clone.style.background = background; if (background !== 'transparent') clone.style.background = background;
if (!clone.getAttribute('xmlns')) clone.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); if (!clone.getAttribute('xmlns')) clone.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml');
const word = el.querySelector("h3").textContent.trim();
const phonetic = el.querySelector(".pronun").textContent.trim();
const [types, ...defs] = [...el.querySelectorAll("ol.defs li")].map(node => node.textContent.trim());
const xhtml = new XMLSerializer().serializeToString(clone); function charsPerLine(text, maxWidth) {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"> const fontSize = Math.round(width * 0.025);
<foreignObject x="0" y="0" width="100%" height="100%">${xhtml}</foreignObject> const canvas = document.createElement("canvas");
</svg>`; const ctx = canvas.getContext("2d");
ctx.font = `${fontSize}px Helvetica Neue, Segoe UI, Helvetica, sans-serif`;
return Math.floor(maxWidth / (ctx.measureText(text).width / text.length));
}
const renderDefs = (defs, x, y) => {
let yOffset = y;
const offset = 20;
const maxLength = charsPerLine(defs[0], width - 60);
return defs.map((def, idx) => {
if (idx > 0) {
yOffset += vOffset;
}
let part = '';
const parts = [];
const words = def.split(' ');
for (const word of words) {
if ((part + ' ' + word).trim().length <= maxLength) {
part = (part + ' ' + word).trim();
} else {
if (part) parts.push(part);
part = word;
}
}
if (part) parts.push(part);
return `
<text class="num" x="0" y="${yOffset}">${idx + 1}.</text>
<text class="def" x="${x}" y="${yOffset}">
${parts.map((part, idx) => {
if (idx > 0) {
yOffset += offset;
}
return `<tspan x="${x}" y="${yOffset}">${part}</tspan>`
}).join('')}
</text>
`;
}).join('');
}
const vOffset = 25;
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
<style>
.card { fill: #ffffff; stroke: #e5e7eb; stroke-width: 1.2; rx: 5; ry: 5; }
.headword {
font-size: ${Math.round(width * 0.03)}px;
font-weight: 700;
fill: #111827;
}
.pron {
font-size: ${Math.round(width * 0.025)}px;
fill: #6b7280;
}
.pos {
font-size: ${Math.round(width * 0.017)}px;
font-weight: bold;
fill: #666;
}
.def {
font-size: ${Math.round(width * 0.025)}px;
fill: #111827;
}
.num {
font-size: ${Math.round(width * 0.025)}px;
fill: #6b7280;
}
text {
font-family: "Helvetica Neue", "Segoe UI", Helvetica, sans-serif;
}
</style>
<!-- Card background -->
<rect x="8" y="8" width="${width - 16}" height="${height - 16}" rx="5" ry="5" class="card"/>
<!-- Content -->
<g transform="translate(32,36)">
<!-- Headword -->
<text class="headword" x="0" y="0">${word}</text>
<!-- Pronunciation -->
<text class="pron" x="0" y="20">${phonetic}</text>
<!-- POS -->
<text class="pos" x="13" y="45">${types}</text>
<!-- Definition 1 -->
${renderDefs(defs, 24, 75)}
</g>
</svg>
`;
const svgBlob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' }); const svgBlob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(svgBlob); const url = URL.createObjectURL(svgBlob);

View File

@@ -642,7 +642,7 @@ button:hover,
/* Autocomplete */ /* Autocomplete */
.autocomp { .autocomp {
background: #f8f8f8; background: #fff;
border-radius: 0 0 5px 5px; border-radius: 0 0 5px 5px;
border: 1px solid #ccc; border: 1px solid #ccc;
border-top: 0; border-top: 0;
@@ -652,9 +652,9 @@ button:hover,
} }
.autocomp-item { .autocomp-item {
padding-bottom: 5px; padding: 5px 10px;
padding: 10px;
cursor: pointer; cursor: pointer;
border-bottom: 1px solid #eee;
} }
.autocomp-item:hover, .autocomp-item:hover,