report-typo 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/en.js.map +1 -1
- package/dist/es.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/uk.js.map +1 -1
- package/package.json +3 -2
package/dist/en.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../locales/en.yaml","../src/capture.ts","../src/dialog.ts","../src/transport/relay.ts","../src/transport/webhook.ts","../src/core.ts","../src/entries/en.ts"],"sourcesContent":["export default {\"dir\":\"ltr\",\"title\":\"Report a typo\",\"send\":\"Send\",\"cancel\":\"Cancel\",\"comment\":\"Comment (optional)\",\"commentPlaceholder\":\"What should it say?\"}","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Draft } from './types.js'\n\n/** Characters of context kept on each side of the selection. */\nexport const CONTEXT_CHARS = 64\n\nconst EDITABLE = 'input,textarea,select,[contenteditable]:not([contenteditable=\"false\"])'\nconst BLOCK =\n 'p,li,dd,dt,td,th,blockquote,pre,figcaption,h1,h2,h3,h4,h5,h6,article,section,main,body'\n\n/** Reports whether a key event is Ctrl+Enter, or Cmd+Enter on macOS. */\nexport function isTrigger(event: Pick<KeyboardEvent, 'ctrlKey' | 'metaKey' | 'key'>): boolean {\n return (event.ctrlKey || event.metaKey) && event.key === 'Enter'\n}\n\n/** Reports whether the event target is a field the reader is typing in. */\nexport function isEditable(target: EventTarget | null): boolean {\n const element = target as Partial<Element> | null\n return Boolean(element?.closest?.(EDITABLE))\n}\n\n/** Bytes report-relay keeps of one free-text field. */\nexport const MAX_TEXT_BYTES = 1024\n\n/** Cuts text to a UTF-8 byte budget on a code point boundary. */\nexport function clipBytes(text: string, max = MAX_TEXT_BYTES): string {\n const encoder = new TextEncoder()\n let clipped = text\n while (encoder.encode(clipped).length > max) clipped = [...clipped].slice(0, -1).join('')\n return clipped\n}\n\nconst squash = (text: string): string => text.replaceAll(/\\s+/g, ' ')\n\n/** Builds a draft from the text before, inside and after a selection, cut to whole words. */\nexport function toDraft(before: string, selected: string, after: string, max: number): Draft {\n const prefix = squash(before)\n const suffix = squash(after)\n const head = prefix.slice(-CONTEXT_CHARS)\n const tail = suffix.slice(0, CONTEXT_CHARS)\n return {\n selection: squash(selected).trim().slice(0, max),\n prefix: (prefix.length > CONTEXT_CHARS ? head.replace(/^\\S*\\s/, '') : head).trimStart(),\n suffix: (suffix.length > CONTEXT_CHARS ? tail.replace(/\\s\\S*$/, '') : tail).trimEnd()\n }\n}\n\n/** Collects the rendered text nodes of a subtree, skipping non-rendered and hidden ones. */\nfunction renderedTextNodes(scope: Node): Text[] {\n const nodes: Text[] = []\n const walker = document.createTreeWalker(scope, 5, {\n acceptNode(node) {\n if (node.nodeType === 3) return 1\n const element = node as Element\n return '|SCRIPT|STYLE|TEMPLATE|NOSCRIPT|'.includes('|' + element.tagName + '|') ||\n element.hasAttribute('hidden') ||\n element.getAttribute('aria-hidden') === 'true'\n ? 2\n : 3\n }\n })\n for (let node = walker.firstChild(); node; node = walker.nextNode()) {\n if (node.nodeType === 3) nodes.push(node as Text)\n }\n return nodes\n}\n\n/** Reads the current selection and its surrounding text from the page. */\nexport function capture(selection: Selection | null, max: number): Draft | undefined {\n if (!selection || selection.isCollapsed || selection.rangeCount === 0) return\n const range = selection.getRangeAt(0)\n const common = range.commonAncestorContainer\n const scope =\n (common instanceof Element ? common : common.parentElement)?.closest(BLOCK) ?? document.body\n\n if (typeof range.comparePoint !== 'function') {\n const before = document.createRange()\n before.selectNodeContents(scope)\n before.setEnd(range.startContainer, range.startOffset)\n const after = document.createRange()\n after.selectNodeContents(scope)\n after.setStart(range.endContainer, range.endOffset)\n const draft = toDraft(before.toString(), range.toString(), after.toString(), max)\n return draft.selection ? draft : undefined\n }\n\n const nodes = renderedTextNodes(scope)\n\n let beforeText = ''\n for (const node of nodes) {\n if (node === range.startContainer) {\n beforeText += node.data.slice(0, range.startOffset)\n break\n }\n if (range.comparePoint(node, node.data.length) < 0) beforeText += node.data\n else break\n }\n\n let selectedText = ''\n for (const node of nodes) {\n if (range.comparePoint(node, node.data.length) < 0) continue\n if (range.comparePoint(node, 0) > 0) break\n let from = 0\n let to = node.data.length\n if (node === range.startContainer) from = range.startOffset\n if (node === range.endContainer) to = range.endOffset\n if (to > from) selectedText += node.data.slice(from, to)\n }\n if (!selectedText) selectedText = range.toString()\n\n let afterText = ''\n let hasStarted = false\n for (const node of nodes) {\n if (hasStarted) {\n afterText += node.data\n } else if (node === range.endContainer) {\n afterText += node.data.slice(range.endOffset)\n hasStarted = true\n } else if (range.comparePoint(node, 0) > 0) {\n afterText += node.data\n hasStarted = true\n }\n }\n\n const draft = toDraft(beforeText, selectedText, afterText, max)\n return draft.selection ? draft : undefined\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport { MAX_TEXT_BYTES } from './capture.js'\nimport type { Confirmation, Draft, Messages } from './types.js'\n\nconst CSS = `dialog{max-inline-size:min(32rem,90vw);font:var(--report-typo-font,1rem/1.5 system-ui,sans-serif);\ncolor:var(--report-typo-color,CanvasText);background:var(--report-typo-background,Canvas);\nborder:var(--report-typo-border,1px solid GrayText);border-radius:var(--report-typo-radius,.5rem)}\np,label{display:block;margin:0 0 1rem}mark{padding:0 .1em;background:var(--report-typo-highlight,Mark)}\ntextarea{display:block;box-sizing:border-box;inline-size:100%;font:inherit}\nmenu{display:flex;gap:.5rem;justify-content:end;margin:0;padding:0}`\n\nconst element = <K extends keyof HTMLElementTagNameMap>(\n tag: K,\n parent: ParentNode,\n text = ''\n): HTMLElementTagNameMap[K] => {\n const node = document.createElement(tag)\n node.textContent = text\n parent.append(node)\n return node\n}\n\n/** Shows the draft in a modal dialog inside a closed shadow root and waits for Send or Cancel. */\nexport function confirm(\n draft: Draft,\n messages: Messages,\n lang: string,\n hasComment: boolean\n): Promise<Confirmation | undefined> {\n const host = document.createElement('report-typo')\n const root = host.attachShadow({ mode: 'closed' })\n const sheet = new CSSStyleSheet()\n sheet.replaceSync(CSS)\n root.adoptedStyleSheets = [sheet]\n const dialog = element('dialog', root)\n dialog.dir = messages.dir\n dialog.lang = lang\n const form = element('form', dialog)\n form.method = 'dialog'\n element('h2', form, messages.title)\n const context = element('p', form, draft.prefix)\n element('mark', context, draft.selection)\n context.append(draft.suffix)\n const label = hasComment ? element('label', form, messages.comment) : undefined\n const comment = label ? element('textarea', label) : undefined\n if (comment) {\n comment.placeholder = messages.commentPlaceholder\n comment.maxLength = MAX_TEXT_BYTES\n comment.rows = 3\n }\n const menu = element('menu', form)\n element('button', menu, messages.cancel).value = 'cancel'\n const send = element('button', menu, messages.send)\n send.value = 'send'\n send.autofocus = !comment\n document.body.append(host)\n return new Promise(resolve => {\n dialog.addEventListener('close', () => {\n host.remove()\n resolve(dialog.returnValue === 'send' ? { comment: comment?.value } : undefined)\n })\n dialog.showModal()\n comment?.focus()\n })\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Send, TypoReport } from '../types.js'\n\n/** Per-attempt deadline of one delivery. */\nexport const TIMEOUT_MS = 5000\n\nconst post = (endpoint: string, report: TypoReport): Promise<Response> =>\n fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/reports+json' },\n body: JSON.stringify([report]),\n keepalive: true,\n credentials: 'omit',\n mode: 'cors',\n signal: AbortSignal.timeout(TIMEOUT_MS)\n })\n\n/** Milliseconds to wait on a `Retry-After` header, with up to 50% jitter; 1 s when absent or a date. */\nexport function retryAfterMs(header: string | null, random = Math.random): number {\n const seconds = Number(header)\n return (Number.isFinite(seconds) && seconds > 0 ? seconds : 1) * 1000 * (1 + random() / 2)\n}\n\n/** Sends reports to a report-relay intake, retrying once on 429. */\nexport function relay(endpoint: string): Send {\n return async report => {\n let response = await post(endpoint, report)\n if (response.status === 429) {\n const wait = retryAfterMs(response.headers.get('Retry-After'))\n console.debug('report-typo: relay busy, retrying', { status: response.status, wait })\n await new Promise(resolve => setTimeout(resolve, wait))\n response = await post(endpoint, report)\n }\n if (!response.ok) throw new Error(`report-relay answered ${response.status}`)\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Send, TypoReport } from '../types.js'\nimport { TIMEOUT_MS } from './relay.js'\n\n/** Turns a report into the JSON a webhook expects. */\nexport type Format = (report: TypoReport) => unknown\n\n/** Sends reports as a plain JSON POST, shaped by `format` when given. */\nexport function webhook(url: string, format: Format = report => report): Send {\n return async report => {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(format(report)),\n keepalive: true,\n credentials: 'omit',\n signal: AbortSignal.timeout(TIMEOUT_MS)\n })\n if (!response.ok) throw new Error(`webhook answered ${response.status}`)\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport { capture, clipBytes, isEditable, isTrigger } from './capture.js'\nimport { confirm } from './dialog.js'\nimport { relay } from './transport/relay.js'\nimport { webhook, type Format } from './transport/webhook.js'\nimport type { Messages, Send, Ui } from './types.js'\n\n/** Options of `reportTypo`; `endpoint` or `send` is required. */\nexport interface Options {\n endpoint?: string\n transport?: 'relay' | 'webhook'\n format?: Format\n send?: Send\n locale?: string\n maxSelection?: number\n cooldownMs?: number\n comment?: boolean\n ui?: Ui\n url?: 'full' | 'path' | 'origin' | ((location: Location) => string)\n}\n\nconst debug = (message: string, data: unknown): void =>\n console.debug(`report-typo: ${message}`, data)\n\n/** Picks the catalog entry for a language tag by its base subtag, falling back to the first entry. */\nexport function pick(catalog: Record<string, Messages>, tag: string): [string, Messages] {\n const base = tag.toLowerCase().split('-', 1)[0] ?? ''\n const found = catalog[base]\n if (found) return [base, found]\n const [first] = Object.entries(catalog)\n if (!first) throw new TypeError('report-typo: empty locale catalog')\n return first\n}\n\n/** Resolves the page URL to send, per the `url` option, dropping the fragment. */\nexport function resolveUrl(\n option: Options['url'],\n location: Location = globalThis.location\n): string {\n const fallback = location.origin + location.pathname\n if (typeof option === 'function') {\n const candidate = option(location)\n let parsed: URL\n try {\n parsed = new URL(candidate)\n } catch {\n console.debug('report-typo: invalid url option, falling back to path', { value: candidate })\n return fallback\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:' || parsed.host === '') {\n console.debug('report-typo: invalid url option, falling back to path', { value: candidate })\n return fallback\n }\n parsed.hash = ''\n if (parsed.search === '?') parsed.search = ''\n return parsed.origin + parsed.pathname + parsed.search\n }\n switch (option) {\n case 'full': {\n const parsed = new URL(location.href)\n parsed.hash = ''\n if (parsed.search === '?') parsed.search = ''\n return parsed.origin + parsed.pathname + parsed.search\n }\n case 'origin': {\n return location.origin\n }\n default: {\n return fallback\n }\n }\n}\n\n/** Builds `reportTypo` over a locale catalog, so each bundle ships only the locales it imports. */\nexport function create(catalog: Record<string, Messages>) {\n return function reportTypo(options: Options): () => void {\n const {\n endpoint,\n transport = 'relay',\n format,\n maxSelection = 256,\n cooldownMs = 2000,\n comment = false,\n url = 'path'\n } = options\n if (!endpoint && !options.send) throw new TypeError('report-typo: set `endpoint` or `send`')\n let send: Send\n if (options.send) send = options.send\n else if (endpoint === undefined) throw new TypeError('report-typo: set `endpoint` or `send`')\n else send = transport === 'webhook' ? webhook(endpoint, format) : relay(endpoint)\n const pageLang = document.documentElement.lang\n const [locale, messages] = pick(catalog, options.locale ?? (pageLang || navigator.language))\n const ui = options.ui ?? (draft => confirm(draft, messages, locale, comment))\n let last = -Infinity\n let isOpen = false\n const onKey = async (event: KeyboardEvent): Promise<void> => {\n if (!isTrigger(event)) return\n if (isEditable(event.target))\n return debug('ignored in an editable field', { target: event.target })\n const draft = capture(getSelection(), maxSelection)\n if (!draft) return debug('ignored without a selection', { maxSelection })\n const wait = last + cooldownMs - Date.now()\n if (wait > 0) return debug('ignored while cooling down', { wait })\n if (isOpen) return debug('ignored while the dialog is open', { selection: draft.selection })\n event.preventDefault()\n isOpen = true\n try {\n const confirmation = await ui(draft)\n if (!confirmation) return debug('cancelled', { selection: draft.selection })\n last = Date.now()\n const note = clipBytes(confirmation.comment?.trim() ?? '')\n const body = { ...draft, lang: pageLang || locale, ...(note && { comment: note }) }\n await send({ type: 'typo', age: 0, url: resolveUrl(url, location), body })\n debug('sent', { selection: draft.selection, transport, comment: Boolean(note) })\n } catch (error) {\n debug('not sent', { error })\n } finally {\n isOpen = false\n }\n }\n const listener = (event: KeyboardEvent): void => void onKey(event)\n document.addEventListener('keydown', listener)\n debug('listening', { locale, transport, cooldownMs })\n return () => {\n document.removeEventListener('keydown', listener)\n debug('disposed', { locale })\n }\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport en from '../../locales/en.yaml'\nimport { create } from '../core.js'\n\nexport type { Options } from '../core.js'\nexport type { Confirmation, Draft, Messages, Send, TypoBody, TypoReport, Ui } from '../types.js'\nexport { relay } from '../transport/relay.js'\nexport { webhook, type Format } from '../transport/webhook.js'\n\n/** Starts listening for Ctrl+Enter and returns the function that stops it. */\nexport const reportTypo = create({ en })\n"],"mappings":"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","names":["en_default","EDITABLE","BLOCK","isTrigger","event","isEditable","target","MAX_TEXT_BYTES","clipBytes","text","max","encoder","clipped","squash","toDraft","before","selected","after","prefix","suffix","head","tail","renderedTextNodes","scope","nodes","walker","node","element","capture","selection","range","common","draft","beforeText","selectedText","from","to","afterText","hasStarted","CSS","element","tag","parent","text","node","confirm","draft","messages","lang","hasComment","host","root","sheet","dialog","form","context","label","comment","MAX_TEXT_BYTES","menu","send","resolve","post","endpoint","report","retryAfterMs","header","random","seconds","relay","response","wait","resolve","webhook","url","format","report","response","debug","message","data","pick","catalog","tag","base","found","first","resolveUrl","option","location","fallback","candidate","parsed","create","options","endpoint","transport","format","maxSelection","cooldownMs","comment","url","send","webhook","relay","pageLang","locale","messages","ui","draft","confirm","last","isOpen","onKey","event","isTrigger","isEditable","capture","wait","confirmation","note","clipBytes","body","error","listener","reportTypo","create","en_default"]}
|
|
1
|
+
{"version":3,"sources":["../locales/en.yaml","../src/capture.ts","../src/dialog.ts","../src/transport/relay.ts","../src/transport/webhook.ts","../src/core.ts","../src/entries/en.ts"],"sourcesContent":["export default {\"dir\":\"ltr\",\"title\":\"Report a typo\",\"send\":\"Send\",\"cancel\":\"Cancel\",\"comment\":\"Comment (optional)\",\"commentPlaceholder\":\"What should it say?\"}","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Draft } from './types.js'\n\n/** Characters of context kept on each side of the selection. */\nexport const CONTEXT_CHARS = 64\n\nconst EDITABLE = 'input,textarea,select,[contenteditable]:not([contenteditable=\"false\"])'\nconst BLOCK =\n 'p,li,dd,dt,td,th,blockquote,pre,figcaption,h1,h2,h3,h4,h5,h6,article,section,main,body'\n\n/** Reports whether a key event is Ctrl+Enter, or Cmd+Enter on macOS. */\nexport function isTrigger(event: Pick<KeyboardEvent, 'ctrlKey' | 'metaKey' | 'key'>): boolean {\n return (event.ctrlKey || event.metaKey) && event.key === 'Enter'\n}\n\n/** Reports whether the event target is a field the reader is typing in. */\nexport function isEditable(target: EventTarget | null): boolean {\n const element = target as Partial<Element> | null\n return Boolean(element?.closest?.(EDITABLE))\n}\n\n/** Bytes report-relay keeps of one free-text field. */\nexport const MAX_TEXT_BYTES = 1024\n\n/** Cuts text to a UTF-8 byte budget on a code point boundary. */\nexport function clipBytes(text: string, max = MAX_TEXT_BYTES): string {\n const encoder = new TextEncoder()\n let clipped = text\n while (encoder.encode(clipped).length > max) clipped = [...clipped].slice(0, -1).join('')\n return clipped\n}\n\nconst squash = (text: string): string => text.replaceAll(/\\s+/g, ' ')\n\n/** Builds a draft from the text before, inside and after a selection, cut to whole words. */\nexport function toDraft(before: string, selected: string, after: string, max: number): Draft {\n const prefix = squash(before)\n const suffix = squash(after)\n const head = prefix.slice(-CONTEXT_CHARS)\n const tail = suffix.slice(0, CONTEXT_CHARS)\n return {\n selection: squash(selected).trim().slice(0, max),\n prefix: (prefix.length > CONTEXT_CHARS ? head.replace(/^\\S*\\s/, '') : head).trimStart(),\n suffix: (suffix.length > CONTEXT_CHARS ? tail.replace(/\\s\\S*$/, '') : tail).trimEnd()\n }\n}\n\n/** Collects the rendered text nodes of a subtree, skipping non-rendered and hidden ones. */\nfunction renderedTextNodes(scope: Node): Text[] {\n const nodes: Text[] = []\n const walker = document.createTreeWalker(scope, 5, {\n acceptNode(node) {\n if (node.nodeType === 3) return 1\n const element = node as Element\n return '|SCRIPT|STYLE|TEMPLATE|NOSCRIPT|'.includes('|' + element.tagName + '|') ||\n element.hasAttribute('hidden') ||\n element.getAttribute('aria-hidden') === 'true'\n ? 2\n : 3\n }\n })\n for (let node = walker.firstChild(); node; node = walker.nextNode()) {\n if (node.nodeType === 3) nodes.push(node as Text)\n }\n return nodes\n}\n\n/** Reads the current selection and its surrounding text from the page. */\nexport function capture(selection: Selection | null, max: number): Draft | undefined {\n if (!selection || selection.isCollapsed || selection.rangeCount === 0) return\n const range = selection.getRangeAt(0)\n const common = range.commonAncestorContainer\n const scope =\n (common instanceof Element ? common : common.parentElement)?.closest(BLOCK) ?? document.body\n\n if (typeof range.comparePoint !== 'function') {\n const before = document.createRange()\n before.selectNodeContents(scope)\n before.setEnd(range.startContainer, range.startOffset)\n const after = document.createRange()\n after.selectNodeContents(scope)\n after.setStart(range.endContainer, range.endOffset)\n const draft = toDraft(before.toString(), range.toString(), after.toString(), max)\n return draft.selection ? draft : undefined\n }\n\n const nodes = renderedTextNodes(scope)\n\n let beforeText = ''\n for (const node of nodes) {\n if (node === range.startContainer) {\n beforeText += node.data.slice(0, range.startOffset)\n break\n }\n if (range.comparePoint(node, node.data.length) < 0) beforeText += node.data\n else break\n }\n\n let selectedText = ''\n for (const node of nodes) {\n if (range.comparePoint(node, node.data.length) < 0) continue\n if (range.comparePoint(node, 0) > 0) break\n let from = 0\n let to = node.data.length\n if (node === range.startContainer) from = range.startOffset\n if (node === range.endContainer) to = range.endOffset\n if (to > from) selectedText += node.data.slice(from, to)\n }\n if (!selectedText) selectedText = range.toString()\n\n let afterText = ''\n let hasStarted = false\n for (const node of nodes) {\n if (hasStarted) {\n afterText += node.data\n } else if (node === range.endContainer) {\n afterText += node.data.slice(range.endOffset)\n hasStarted = true\n } else if (range.comparePoint(node, 0) > 0) {\n afterText += node.data\n hasStarted = true\n }\n }\n\n const draft = toDraft(beforeText, selectedText, afterText, max)\n return draft.selection ? draft : undefined\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport { MAX_TEXT_BYTES } from './capture.js'\nimport type { Confirmation, Draft, Messages } from './types.js'\n\nconst CSS = `dialog{max-inline-size:min(32rem,90vw);font:var(--report-typo-font,1rem/1.5 system-ui,sans-serif);\ncolor:var(--report-typo-color,CanvasText);background:var(--report-typo-background,Canvas);\nborder:var(--report-typo-border,1px solid GrayText);border-radius:var(--report-typo-radius,.5rem)}\np,label{display:block;margin:0 0 1rem}mark{padding:0 .1em;background:var(--report-typo-highlight,Mark)}\ntextarea{display:block;box-sizing:border-box;inline-size:100%;font:inherit}\nmenu{display:flex;gap:.5rem;justify-content:end;margin:0;padding:0}`\n\nconst element = <K extends keyof HTMLElementTagNameMap>(\n tag: K,\n parent: ParentNode,\n text = ''\n): HTMLElementTagNameMap[K] => {\n const node = document.createElement(tag)\n node.textContent = text\n parent.append(node)\n return node\n}\n\n/** Shows the draft in a modal dialog inside a closed shadow root and waits for Send or Cancel. */\nexport function confirm(\n draft: Draft,\n messages: Messages,\n lang: string,\n hasComment: boolean\n): Promise<Confirmation | undefined> {\n const host = document.createElement('report-typo')\n const root = host.attachShadow({ mode: 'closed' })\n const sheet = new CSSStyleSheet()\n sheet.replaceSync(CSS)\n root.adoptedStyleSheets = [sheet]\n const dialog = element('dialog', root)\n dialog.dir = messages.dir\n dialog.lang = lang\n const form = element('form', dialog)\n form.method = 'dialog'\n element('h2', form, messages.title)\n const context = element('p', form, draft.prefix)\n element('mark', context, draft.selection)\n context.append(draft.suffix)\n const label = hasComment ? element('label', form, messages.comment) : undefined\n const comment = label ? element('textarea', label) : undefined\n if (comment) {\n comment.placeholder = messages.commentPlaceholder\n comment.maxLength = MAX_TEXT_BYTES\n comment.rows = 3\n }\n const menu = element('menu', form)\n element('button', menu, messages.cancel).value = 'cancel'\n const send = element('button', menu, messages.send)\n send.value = 'send'\n send.autofocus = !comment\n document.body.append(host)\n return new Promise(resolve => {\n dialog.addEventListener('close', () => {\n host.remove()\n resolve(dialog.returnValue === 'send' ? { comment: comment?.value } : undefined)\n })\n dialog.showModal()\n comment?.focus()\n })\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Send, TypoReport } from '../types.js'\n\n/** Per-attempt deadline of one delivery. */\nexport const TIMEOUT_MS = 5000\n\nconst post = (endpoint: string, report: TypoReport): Promise<Response> =>\n fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/reports+json' },\n body: JSON.stringify([report]),\n keepalive: true,\n credentials: 'omit',\n mode: 'cors',\n signal: AbortSignal.timeout(TIMEOUT_MS)\n })\n\n/** Milliseconds to wait on a `Retry-After` header, with up to 50% jitter; 1 s when absent or a date. */\nexport function retryAfterMs(header: string | null, random = Math.random): number {\n const seconds = Number(header)\n return (Number.isFinite(seconds) && seconds > 0 ? seconds : 1) * 1000 * (1 + random() / 2)\n}\n\n/** Sends reports to a report-relay intake, retrying once on 429. */\nexport function relay(endpoint: string): Send {\n return async report => {\n let response = await post(endpoint, report)\n if (response.status === 429) {\n const wait = retryAfterMs(response.headers.get('Retry-After'))\n console.debug('report-typo: relay busy, retrying', { status: response.status, wait })\n await new Promise(resolve => setTimeout(resolve, wait))\n response = await post(endpoint, report)\n }\n if (!response.ok) throw new Error(`report-relay answered ${response.status}`)\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Send, TypoReport } from '../types.js'\nimport { TIMEOUT_MS } from './relay.js'\n\n/** Turns a report into the JSON a webhook expects. */\nexport type Format = (report: TypoReport) => unknown\n\n/** Sends reports as a plain JSON POST, shaped by `format` when given. */\nexport function webhook(url: string, format: Format = report => report): Send {\n return async report => {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(format(report)),\n keepalive: true,\n credentials: 'omit',\n signal: AbortSignal.timeout(TIMEOUT_MS)\n })\n if (!response.ok) throw new Error(`webhook answered ${response.status}`)\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport { capture, clipBytes, isEditable, isTrigger } from './capture.js'\nimport { confirm } from './dialog.js'\nimport { relay } from './transport/relay.js'\nimport { webhook, type Format } from './transport/webhook.js'\nimport type { Messages, Send, Ui } from './types.js'\n\n/** Options of `reportTypo`; `endpoint` or `send` is required. */\nexport interface Options {\n endpoint?: string\n transport?: 'relay' | 'webhook'\n format?: Format\n send?: Send\n locale?: string\n maxSelection?: number\n cooldownMs?: number\n comment?: boolean\n ui?: Ui\n url?: 'full' | 'path' | 'origin' | ((location: Location) => string)\n}\n\nconst debug = (message: string, data: unknown): void =>\n console.debug(`report-typo: ${message}`, data)\n\n/** Picks the catalog entry for a language tag by its base subtag, falling back to the first entry. */\nexport function pick(catalog: Record<string, Messages>, tag: string): [string, Messages] {\n const base = tag.toLowerCase().split('-', 1)[0] ?? ''\n const found = catalog[base]\n if (found) return [base, found]\n const [first] = Object.entries(catalog)\n if (!first) throw new TypeError('report-typo: empty locale catalog')\n return first\n}\n\n/** Resolves the page URL to send, per the `url` option, dropping the fragment. */\nexport function resolveUrl(\n option: Options['url'],\n location: Location = globalThis.location\n): string {\n const fallback = location.origin + location.pathname\n if (typeof option === 'function') {\n const candidate = option(location)\n let parsed: URL\n try {\n parsed = new URL(candidate)\n } catch {\n console.debug('report-typo: invalid url option, falling back to path', { value: candidate })\n return fallback\n }\n if ((parsed.protocol !== 'http:' && parsed.protocol !== 'https:') || parsed.host === '') {\n console.debug('report-typo: invalid url option, falling back to path', { value: candidate })\n return fallback\n }\n parsed.hash = ''\n if (parsed.search === '?') parsed.search = ''\n return parsed.origin + parsed.pathname + parsed.search\n }\n switch (option) {\n case 'full': {\n const parsed = new URL(location.href)\n parsed.hash = ''\n if (parsed.search === '?') parsed.search = ''\n return parsed.origin + parsed.pathname + parsed.search\n }\n case 'origin': {\n return location.origin\n }\n default: {\n return fallback\n }\n }\n}\n\n/** Builds `reportTypo` over a locale catalog, so each bundle ships only the locales it imports. */\nexport function create(catalog: Record<string, Messages>) {\n return function reportTypo(options: Options): () => void {\n const {\n endpoint,\n transport = 'relay',\n format,\n maxSelection = 256,\n cooldownMs = 2000,\n comment = false,\n url = 'path'\n } = options\n if (!endpoint && !options.send) throw new TypeError('report-typo: set `endpoint` or `send`')\n let send: Send\n if (options.send) send = options.send\n else if (endpoint === undefined) throw new TypeError('report-typo: set `endpoint` or `send`')\n else send = transport === 'webhook' ? webhook(endpoint, format) : relay(endpoint)\n const pageLang = document.documentElement.lang\n const [locale, messages] = pick(catalog, options.locale ?? (pageLang || navigator.language))\n const ui = options.ui ?? (draft => confirm(draft, messages, locale, comment))\n let last = -Infinity\n let isOpen = false\n const onKey = async (event: KeyboardEvent): Promise<void> => {\n if (!isTrigger(event)) return\n if (isEditable(event.target))\n return debug('ignored in an editable field', { target: event.target })\n const draft = capture(getSelection(), maxSelection)\n if (!draft) return debug('ignored without a selection', { maxSelection })\n const wait = last + cooldownMs - Date.now()\n if (wait > 0) return debug('ignored while cooling down', { wait })\n if (isOpen) return debug('ignored while the dialog is open', { selection: draft.selection })\n event.preventDefault()\n isOpen = true\n try {\n const confirmation = await ui(draft)\n if (!confirmation) return debug('cancelled', { selection: draft.selection })\n last = Date.now()\n const note = clipBytes(confirmation.comment?.trim() ?? '')\n const body = { ...draft, lang: pageLang || locale, ...(note && { comment: note }) }\n await send({ type: 'typo', age: 0, url: resolveUrl(url, location), body })\n debug('sent', { selection: draft.selection, transport, comment: Boolean(note) })\n } catch (error) {\n debug('not sent', { error })\n } finally {\n isOpen = false\n }\n }\n const listener = (event: KeyboardEvent): void => void onKey(event)\n document.addEventListener('keydown', listener)\n debug('listening', { locale, transport, cooldownMs })\n return () => {\n document.removeEventListener('keydown', listener)\n debug('disposed', { locale })\n }\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport en from '../../locales/en.yaml'\nimport { create } from '../core.js'\n\nexport type { Options } from '../core.js'\nexport type { Confirmation, Draft, Messages, Send, TypoBody, TypoReport, Ui } from '../types.js'\nexport { relay } from '../transport/relay.js'\nexport { webhook, type Format } from '../transport/webhook.js'\n\n/** Starts listening for Ctrl+Enter and returns the function that stops it. */\nexport const reportTypo = create({ en })\n"],"mappings":"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","names":["en_default","EDITABLE","BLOCK","isTrigger","event","isEditable","target","MAX_TEXT_BYTES","clipBytes","text","max","encoder","clipped","squash","toDraft","before","selected","after","prefix","suffix","head","tail","renderedTextNodes","scope","nodes","walker","node","element","capture","selection","range","common","draft","beforeText","selectedText","from","to","afterText","hasStarted","CSS","element","tag","parent","text","node","confirm","draft","messages","lang","hasComment","host","root","sheet","dialog","form","context","label","comment","MAX_TEXT_BYTES","menu","send","resolve","post","endpoint","report","retryAfterMs","header","random","seconds","relay","response","wait","resolve","webhook","url","format","report","response","debug","message","data","pick","catalog","tag","base","found","first","resolveUrl","option","location","fallback","candidate","parsed","create","options","endpoint","transport","format","maxSelection","cooldownMs","comment","url","send","webhook","relay","pageLang","locale","messages","ui","draft","confirm","last","isOpen","onKey","event","isTrigger","isEditable","capture","wait","confirmation","note","clipBytes","body","error","listener","reportTypo","create","en_default"]}
|
package/dist/es.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../locales/es.yaml","../src/capture.ts","../src/dialog.ts","../src/transport/relay.ts","../src/transport/webhook.ts","../src/core.ts","../src/entries/es.ts"],"sourcesContent":["export default {\"dir\":\"ltr\",\"title\":\"Avisar de una errata\",\"send\":\"Enviar\",\"cancel\":\"Cancelar\",\"comment\":\"Comentario (opcional)\",\"commentPlaceholder\":\"¿Qué debería decir?\"}","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Draft } from './types.js'\n\n/** Characters of context kept on each side of the selection. */\nexport const CONTEXT_CHARS = 64\n\nconst EDITABLE = 'input,textarea,select,[contenteditable]:not([contenteditable=\"false\"])'\nconst BLOCK =\n 'p,li,dd,dt,td,th,blockquote,pre,figcaption,h1,h2,h3,h4,h5,h6,article,section,main,body'\n\n/** Reports whether a key event is Ctrl+Enter, or Cmd+Enter on macOS. */\nexport function isTrigger(event: Pick<KeyboardEvent, 'ctrlKey' | 'metaKey' | 'key'>): boolean {\n return (event.ctrlKey || event.metaKey) && event.key === 'Enter'\n}\n\n/** Reports whether the event target is a field the reader is typing in. */\nexport function isEditable(target: EventTarget | null): boolean {\n const element = target as Partial<Element> | null\n return Boolean(element?.closest?.(EDITABLE))\n}\n\n/** Bytes report-relay keeps of one free-text field. */\nexport const MAX_TEXT_BYTES = 1024\n\n/** Cuts text to a UTF-8 byte budget on a code point boundary. */\nexport function clipBytes(text: string, max = MAX_TEXT_BYTES): string {\n const encoder = new TextEncoder()\n let clipped = text\n while (encoder.encode(clipped).length > max) clipped = [...clipped].slice(0, -1).join('')\n return clipped\n}\n\nconst squash = (text: string): string => text.replaceAll(/\\s+/g, ' ')\n\n/** Builds a draft from the text before, inside and after a selection, cut to whole words. */\nexport function toDraft(before: string, selected: string, after: string, max: number): Draft {\n const prefix = squash(before)\n const suffix = squash(after)\n const head = prefix.slice(-CONTEXT_CHARS)\n const tail = suffix.slice(0, CONTEXT_CHARS)\n return {\n selection: squash(selected).trim().slice(0, max),\n prefix: (prefix.length > CONTEXT_CHARS ? head.replace(/^\\S*\\s/, '') : head).trimStart(),\n suffix: (suffix.length > CONTEXT_CHARS ? tail.replace(/\\s\\S*$/, '') : tail).trimEnd()\n }\n}\n\n/** Collects the rendered text nodes of a subtree, skipping non-rendered and hidden ones. */\nfunction renderedTextNodes(scope: Node): Text[] {\n const nodes: Text[] = []\n const walker = document.createTreeWalker(scope, 5, {\n acceptNode(node) {\n if (node.nodeType === 3) return 1\n const element = node as Element\n return '|SCRIPT|STYLE|TEMPLATE|NOSCRIPT|'.includes('|' + element.tagName + '|') ||\n element.hasAttribute('hidden') ||\n element.getAttribute('aria-hidden') === 'true'\n ? 2\n : 3\n }\n })\n for (let node = walker.firstChild(); node; node = walker.nextNode()) {\n if (node.nodeType === 3) nodes.push(node as Text)\n }\n return nodes\n}\n\n/** Reads the current selection and its surrounding text from the page. */\nexport function capture(selection: Selection | null, max: number): Draft | undefined {\n if (!selection || selection.isCollapsed || selection.rangeCount === 0) return\n const range = selection.getRangeAt(0)\n const common = range.commonAncestorContainer\n const scope =\n (common instanceof Element ? common : common.parentElement)?.closest(BLOCK) ?? document.body\n\n if (typeof range.comparePoint !== 'function') {\n const before = document.createRange()\n before.selectNodeContents(scope)\n before.setEnd(range.startContainer, range.startOffset)\n const after = document.createRange()\n after.selectNodeContents(scope)\n after.setStart(range.endContainer, range.endOffset)\n const draft = toDraft(before.toString(), range.toString(), after.toString(), max)\n return draft.selection ? draft : undefined\n }\n\n const nodes = renderedTextNodes(scope)\n\n let beforeText = ''\n for (const node of nodes) {\n if (node === range.startContainer) {\n beforeText += node.data.slice(0, range.startOffset)\n break\n }\n if (range.comparePoint(node, node.data.length) < 0) beforeText += node.data\n else break\n }\n\n let selectedText = ''\n for (const node of nodes) {\n if (range.comparePoint(node, node.data.length) < 0) continue\n if (range.comparePoint(node, 0) > 0) break\n let from = 0\n let to = node.data.length\n if (node === range.startContainer) from = range.startOffset\n if (node === range.endContainer) to = range.endOffset\n if (to > from) selectedText += node.data.slice(from, to)\n }\n if (!selectedText) selectedText = range.toString()\n\n let afterText = ''\n let hasStarted = false\n for (const node of nodes) {\n if (hasStarted) {\n afterText += node.data\n } else if (node === range.endContainer) {\n afterText += node.data.slice(range.endOffset)\n hasStarted = true\n } else if (range.comparePoint(node, 0) > 0) {\n afterText += node.data\n hasStarted = true\n }\n }\n\n const draft = toDraft(beforeText, selectedText, afterText, max)\n return draft.selection ? draft : undefined\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport { MAX_TEXT_BYTES } from './capture.js'\nimport type { Confirmation, Draft, Messages } from './types.js'\n\nconst CSS = `dialog{max-inline-size:min(32rem,90vw);font:var(--report-typo-font,1rem/1.5 system-ui,sans-serif);\ncolor:var(--report-typo-color,CanvasText);background:var(--report-typo-background,Canvas);\nborder:var(--report-typo-border,1px solid GrayText);border-radius:var(--report-typo-radius,.5rem)}\np,label{display:block;margin:0 0 1rem}mark{padding:0 .1em;background:var(--report-typo-highlight,Mark)}\ntextarea{display:block;box-sizing:border-box;inline-size:100%;font:inherit}\nmenu{display:flex;gap:.5rem;justify-content:end;margin:0;padding:0}`\n\nconst element = <K extends keyof HTMLElementTagNameMap>(\n tag: K,\n parent: ParentNode,\n text = ''\n): HTMLElementTagNameMap[K] => {\n const node = document.createElement(tag)\n node.textContent = text\n parent.append(node)\n return node\n}\n\n/** Shows the draft in a modal dialog inside a closed shadow root and waits for Send or Cancel. */\nexport function confirm(\n draft: Draft,\n messages: Messages,\n lang: string,\n hasComment: boolean\n): Promise<Confirmation | undefined> {\n const host = document.createElement('report-typo')\n const root = host.attachShadow({ mode: 'closed' })\n const sheet = new CSSStyleSheet()\n sheet.replaceSync(CSS)\n root.adoptedStyleSheets = [sheet]\n const dialog = element('dialog', root)\n dialog.dir = messages.dir\n dialog.lang = lang\n const form = element('form', dialog)\n form.method = 'dialog'\n element('h2', form, messages.title)\n const context = element('p', form, draft.prefix)\n element('mark', context, draft.selection)\n context.append(draft.suffix)\n const label = hasComment ? element('label', form, messages.comment) : undefined\n const comment = label ? element('textarea', label) : undefined\n if (comment) {\n comment.placeholder = messages.commentPlaceholder\n comment.maxLength = MAX_TEXT_BYTES\n comment.rows = 3\n }\n const menu = element('menu', form)\n element('button', menu, messages.cancel).value = 'cancel'\n const send = element('button', menu, messages.send)\n send.value = 'send'\n send.autofocus = !comment\n document.body.append(host)\n return new Promise(resolve => {\n dialog.addEventListener('close', () => {\n host.remove()\n resolve(dialog.returnValue === 'send' ? { comment: comment?.value } : undefined)\n })\n dialog.showModal()\n comment?.focus()\n })\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Send, TypoReport } from '../types.js'\n\n/** Per-attempt deadline of one delivery. */\nexport const TIMEOUT_MS = 5000\n\nconst post = (endpoint: string, report: TypoReport): Promise<Response> =>\n fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/reports+json' },\n body: JSON.stringify([report]),\n keepalive: true,\n credentials: 'omit',\n mode: 'cors',\n signal: AbortSignal.timeout(TIMEOUT_MS)\n })\n\n/** Milliseconds to wait on a `Retry-After` header, with up to 50% jitter; 1 s when absent or a date. */\nexport function retryAfterMs(header: string | null, random = Math.random): number {\n const seconds = Number(header)\n return (Number.isFinite(seconds) && seconds > 0 ? seconds : 1) * 1000 * (1 + random() / 2)\n}\n\n/** Sends reports to a report-relay intake, retrying once on 429. */\nexport function relay(endpoint: string): Send {\n return async report => {\n let response = await post(endpoint, report)\n if (response.status === 429) {\n const wait = retryAfterMs(response.headers.get('Retry-After'))\n console.debug('report-typo: relay busy, retrying', { status: response.status, wait })\n await new Promise(resolve => setTimeout(resolve, wait))\n response = await post(endpoint, report)\n }\n if (!response.ok) throw new Error(`report-relay answered ${response.status}`)\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Send, TypoReport } from '../types.js'\nimport { TIMEOUT_MS } from './relay.js'\n\n/** Turns a report into the JSON a webhook expects. */\nexport type Format = (report: TypoReport) => unknown\n\n/** Sends reports as a plain JSON POST, shaped by `format` when given. */\nexport function webhook(url: string, format: Format = report => report): Send {\n return async report => {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(format(report)),\n keepalive: true,\n credentials: 'omit',\n signal: AbortSignal.timeout(TIMEOUT_MS)\n })\n if (!response.ok) throw new Error(`webhook answered ${response.status}`)\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport { capture, clipBytes, isEditable, isTrigger } from './capture.js'\nimport { confirm } from './dialog.js'\nimport { relay } from './transport/relay.js'\nimport { webhook, type Format } from './transport/webhook.js'\nimport type { Messages, Send, Ui } from './types.js'\n\n/** Options of `reportTypo`; `endpoint` or `send` is required. */\nexport interface Options {\n endpoint?: string\n transport?: 'relay' | 'webhook'\n format?: Format\n send?: Send\n locale?: string\n maxSelection?: number\n cooldownMs?: number\n comment?: boolean\n ui?: Ui\n url?: 'full' | 'path' | 'origin' | ((location: Location) => string)\n}\n\nconst debug = (message: string, data: unknown): void =>\n console.debug(`report-typo: ${message}`, data)\n\n/** Picks the catalog entry for a language tag by its base subtag, falling back to the first entry. */\nexport function pick(catalog: Record<string, Messages>, tag: string): [string, Messages] {\n const base = tag.toLowerCase().split('-', 1)[0] ?? ''\n const found = catalog[base]\n if (found) return [base, found]\n const [first] = Object.entries(catalog)\n if (!first) throw new TypeError('report-typo: empty locale catalog')\n return first\n}\n\n/** Resolves the page URL to send, per the `url` option, dropping the fragment. */\nexport function resolveUrl(\n option: Options['url'],\n location: Location = globalThis.location\n): string {\n const fallback = location.origin + location.pathname\n if (typeof option === 'function') {\n const candidate = option(location)\n let parsed: URL\n try {\n parsed = new URL(candidate)\n } catch {\n console.debug('report-typo: invalid url option, falling back to path', { value: candidate })\n return fallback\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:' || parsed.host === '') {\n console.debug('report-typo: invalid url option, falling back to path', { value: candidate })\n return fallback\n }\n parsed.hash = ''\n if (parsed.search === '?') parsed.search = ''\n return parsed.origin + parsed.pathname + parsed.search\n }\n switch (option) {\n case 'full': {\n const parsed = new URL(location.href)\n parsed.hash = ''\n if (parsed.search === '?') parsed.search = ''\n return parsed.origin + parsed.pathname + parsed.search\n }\n case 'origin': {\n return location.origin\n }\n default: {\n return fallback\n }\n }\n}\n\n/** Builds `reportTypo` over a locale catalog, so each bundle ships only the locales it imports. */\nexport function create(catalog: Record<string, Messages>) {\n return function reportTypo(options: Options): () => void {\n const {\n endpoint,\n transport = 'relay',\n format,\n maxSelection = 256,\n cooldownMs = 2000,\n comment = false,\n url = 'path'\n } = options\n if (!endpoint && !options.send) throw new TypeError('report-typo: set `endpoint` or `send`')\n let send: Send\n if (options.send) send = options.send\n else if (endpoint === undefined) throw new TypeError('report-typo: set `endpoint` or `send`')\n else send = transport === 'webhook' ? webhook(endpoint, format) : relay(endpoint)\n const pageLang = document.documentElement.lang\n const [locale, messages] = pick(catalog, options.locale ?? (pageLang || navigator.language))\n const ui = options.ui ?? (draft => confirm(draft, messages, locale, comment))\n let last = -Infinity\n let isOpen = false\n const onKey = async (event: KeyboardEvent): Promise<void> => {\n if (!isTrigger(event)) return\n if (isEditable(event.target))\n return debug('ignored in an editable field', { target: event.target })\n const draft = capture(getSelection(), maxSelection)\n if (!draft) return debug('ignored without a selection', { maxSelection })\n const wait = last + cooldownMs - Date.now()\n if (wait > 0) return debug('ignored while cooling down', { wait })\n if (isOpen) return debug('ignored while the dialog is open', { selection: draft.selection })\n event.preventDefault()\n isOpen = true\n try {\n const confirmation = await ui(draft)\n if (!confirmation) return debug('cancelled', { selection: draft.selection })\n last = Date.now()\n const note = clipBytes(confirmation.comment?.trim() ?? '')\n const body = { ...draft, lang: pageLang || locale, ...(note && { comment: note }) }\n await send({ type: 'typo', age: 0, url: resolveUrl(url, location), body })\n debug('sent', { selection: draft.selection, transport, comment: Boolean(note) })\n } catch (error) {\n debug('not sent', { error })\n } finally {\n isOpen = false\n }\n }\n const listener = (event: KeyboardEvent): void => void onKey(event)\n document.addEventListener('keydown', listener)\n debug('listening', { locale, transport, cooldownMs })\n return () => {\n document.removeEventListener('keydown', listener)\n debug('disposed', { locale })\n }\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport es from '../../locales/es.yaml'\nimport { create } from '../core.js'\n\nexport type { Options } from '../core.js'\nexport type { Confirmation, Draft, Messages, Send, TypoBody, TypoReport, Ui } from '../types.js'\nexport { relay } from '../transport/relay.js'\nexport { webhook, type Format } from '../transport/webhook.js'\n\n/** Starts listening for Ctrl+Enter and returns the function that stops it. */\nexport const reportTypo = create({ es })\n"],"mappings":"AAAA,IAAOA,EAAQ,CAAC,IAAM,MAAM,MAAQ,uBAAuB,KAAO,SAAS,OAAS,WAAW,QAAU,wBAAwB,mBAAqB,8BAAqB,ECS3K,IAAMC,EAAW,yEACXC,EACJ,yFAGK,SAASC,EAAUC,EAAoE,CAC5F,OAAQA,EAAM,SAAWA,EAAM,UAAYA,EAAM,MAAQ,OAC3D,CAGO,SAASC,EAAWC,EAAqC,CAE9D,MAAO,EADSA,GACQ,UAAUL,CAAQ,CAC5C,CAGO,IAAMM,EAAiB,KAGvB,SAASC,EAAUC,EAAcC,EAAMH,EAAwB,CACpE,IAAMI,EAAU,IAAI,YAChBC,EAAUH,EACd,KAAOE,EAAQ,OAAOC,CAAO,EAAE,OAASF,GAAKE,EAAU,CAAC,GAAGA,CAAO,EAAE,MAAM,EAAG,EAAE,EAAE,KAAK,EAAE,EACxF,OAAOA,CACT,CAEA,IAAMC,EAAUJ,GAAyBA,EAAK,WAAW,OAAQ,GAAG,EAG7D,SAASK,EAAQC,EAAgBC,EAAkBC,EAAeP,EAAoB,CAC3F,IAAMQ,EAASL,EAAOE,CAAM,EACtBI,EAASN,EAAOI,CAAK,EACrBG,EAAOF,EAAO,MAAM,GAAc,EAClCG,EAAOF,EAAO,MAAM,EAAG,EAAa,EAC1C,MAAO,CACL,UAAWN,EAAOG,CAAQ,EAAE,KAAK,EAAE,MAAM,EAAGN,CAAG,EAC/C,QAASQ,EAAO,OAAS,GAAgBE,EAAK,QAAQ,SAAU,EAAE,EAAIA,GAAM,UAAU,EACtF,QAASD,EAAO,OAAS,GAAgBE,EAAK,QAAQ,SAAU,EAAE,EAAIA,GAAM,QAAQ,CACtF,CACF,CAGA,SAASC,EAAkBC,EAAqB,CAC9C,IAAMC,EAAgB,CAAC,EACjBC,EAAS,SAAS,iBAAiBF,EAAO,EAAG,CACjD,WAAWG,EAAM,CACf,GAAIA,EAAK,WAAa,EAAG,MAAO,GAChC,IAAMC,EAAUD,EAChB,MAAO,mCAAmC,SAAS,IAAMC,EAAQ,QAAU,GAAG,GAC5EA,EAAQ,aAAa,QAAQ,GAC7BA,EAAQ,aAAa,aAAa,IAAM,OACtC,EACA,CACN,CACF,CAAC,EACD,QAASD,EAAOD,EAAO,WAAW,EAAGC,EAAMA,EAAOD,EAAO,SAAS,EAC5DC,EAAK,WAAa,GAAGF,EAAM,KAAKE,CAAY,EAElD,OAAOF,CACT,CAGO,SAASI,EAAQC,EAA6BnB,EAAgC,CACnF,GAAI,CAACmB,GAAaA,EAAU,aAAeA,EAAU,aAAe,EAAG,OACvE,IAAMC,EAAQD,EAAU,WAAW,CAAC,EAC9BE,EAASD,EAAM,wBACfP,GACHQ,aAAkB,QAAUA,EAASA,EAAO,gBAAgB,QAAQ7B,CAAK,GAAK,SAAS,KAE1F,GAAI,OAAO4B,EAAM,cAAiB,WAAY,CAC5C,IAAMf,EAAS,SAAS,YAAY,EACpCA,EAAO,mBAAmBQ,CAAK,EAC/BR,EAAO,OAAOe,EAAM,eAAgBA,EAAM,WAAW,EACrD,IAAMb,EAAQ,SAAS,YAAY,EACnCA,EAAM,mBAAmBM,CAAK,EAC9BN,EAAM,SAASa,EAAM,aAAcA,EAAM,SAAS,EAClD,IAAME,EAAQlB,EAAQC,EAAO,SAAS,EAAGe,EAAM,SAAS,EAAGb,EAAM,SAAS,EAAGP,CAAG,EAChF,OAAOsB,EAAM,UAAYA,EAAQ,MACnC,CAEA,IAAMR,EAAQF,EAAkBC,CAAK,EAEjCU,EAAa,GACjB,QAAWP,KAAQF,EAAO,CACxB,GAAIE,IAASI,EAAM,eAAgB,CACjCG,GAAcP,EAAK,KAAK,MAAM,EAAGI,EAAM,WAAW,EAClD,KACF,CACA,GAAIA,EAAM,aAAaJ,EAAMA,EAAK,KAAK,MAAM,EAAI,EAAGO,GAAcP,EAAK,SAClE,MACP,CAEA,IAAIQ,EAAe,GACnB,QAAWR,KAAQF,EAAO,CACxB,GAAIM,EAAM,aAAaJ,EAAMA,EAAK,KAAK,MAAM,EAAI,EAAG,SACpD,GAAII,EAAM,aAAaJ,EAAM,CAAC,EAAI,EAAG,MACrC,IAAIS,EAAO,EACPC,EAAKV,EAAK,KAAK,OACfA,IAASI,EAAM,iBAAgBK,EAAOL,EAAM,aAC5CJ,IAASI,EAAM,eAAcM,EAAKN,EAAM,WACxCM,EAAKD,IAAMD,GAAgBR,EAAK,KAAK,MAAMS,EAAMC,CAAE,EACzD,CACKF,IAAcA,EAAeJ,EAAM,SAAS,GAEjD,IAAIO,EAAY,GACZC,EAAa,GACjB,QAAWZ,KAAQF,EACbc,EACFD,GAAaX,EAAK,KACTA,IAASI,EAAM,cACxBO,GAAaX,EAAK,KAAK,MAAMI,EAAM,SAAS,EAC5CQ,EAAa,IACJR,EAAM,aAAaJ,EAAM,CAAC,EAAI,IACvCW,GAAaX,EAAK,KAClBY,EAAa,IAIjB,IAAMN,EAAQlB,EAAQmB,EAAYC,EAAcG,EAAW3B,CAAG,EAC9D,OAAOsB,EAAM,UAAYA,EAAQ,MACnC,CC1HA,IAAMO,EAAM;AAAA;AAAA;AAAA;AAAA;AAAA,qEAONC,EAAU,CACdC,EACAC,EACAC,EAAO,KACsB,CAC7B,IAAMC,EAAO,SAAS,cAAcH,CAAG,EACvC,OAAAG,EAAK,YAAcD,EACnBD,EAAO,OAAOE,CAAI,EACXA,CACT,EAGO,SAASC,EACdC,EACAC,EACAC,EACAC,EACmC,CACnC,IAAMC,EAAO,SAAS,cAAc,aAAa,EAC3CC,EAAOD,EAAK,aAAa,CAAE,KAAM,QAAS,CAAC,EAC3CE,EAAQ,IAAI,cAClBA,EAAM,YAAYb,CAAG,EACrBY,EAAK,mBAAqB,CAACC,CAAK,EAChC,IAAMC,EAASb,EAAQ,SAAUW,CAAI,EACrCE,EAAO,IAAMN,EAAS,IACtBM,EAAO,KAAOL,EACd,IAAMM,EAAOd,EAAQ,OAAQa,CAAM,EACnCC,EAAK,OAAS,SACdd,EAAQ,KAAMc,EAAMP,EAAS,KAAK,EAClC,IAAMQ,EAAUf,EAAQ,IAAKc,EAAMR,EAAM,MAAM,EAC/CN,EAAQ,OAAQe,EAAST,EAAM,SAAS,EACxCS,EAAQ,OAAOT,EAAM,MAAM,EAC3B,IAAMU,EAAQP,EAAaT,EAAQ,QAASc,EAAMP,EAAS,OAAO,EAAI,OAChEU,EAAUD,EAAQhB,EAAQ,WAAYgB,CAAK,EAAI,OACjDC,IACFA,EAAQ,YAAcV,EAAS,mBAC/BU,EAAQ,UAAYC,EACpBD,EAAQ,KAAO,GAEjB,IAAME,EAAOnB,EAAQ,OAAQc,CAAI,EACjCd,EAAQ,SAAUmB,EAAMZ,EAAS,MAAM,EAAE,MAAQ,SACjD,IAAMa,EAAOpB,EAAQ,SAAUmB,EAAMZ,EAAS,IAAI,EAClD,OAAAa,EAAK,MAAQ,OACbA,EAAK,UAAY,CAACH,EAClB,SAAS,KAAK,OAAOP,CAAI,EAClB,IAAI,QAAQW,GAAW,CAC5BR,EAAO,iBAAiB,QAAS,IAAM,CACrCH,EAAK,OAAO,EACZW,EAAQR,EAAO,cAAgB,OAAS,CAAE,QAASI,GAAS,KAAM,EAAI,MAAS,CACjF,CAAC,EACDJ,EAAO,UAAU,EACjBI,GAAS,MAAM,CACjB,CAAC,CACH,CC1DA,IAAMK,EAAO,CAACC,EAAkBC,IAC9B,MAAMD,EAAU,CACd,OAAQ,OACR,QAAS,CAAE,eAAgB,0BAA2B,EACtD,KAAM,KAAK,UAAU,CAACC,CAAM,CAAC,EAC7B,UAAW,GACX,YAAa,OACb,KAAM,OACN,OAAQ,YAAY,QAAQ,GAAU,CACxC,CAAC,EAGI,SAASC,EAAaC,EAAuBC,EAAS,KAAK,OAAgB,CAChF,IAAMC,EAAU,OAAOF,CAAM,EAC7B,OAAQ,OAAO,SAASE,CAAO,GAAKA,EAAU,EAAIA,EAAU,GAAK,KAAQ,EAAID,EAAO,EAAI,EAC1F,CAGO,SAASE,EAAMN,EAAwB,CAC5C,MAAO,OAAMC,GAAU,CACrB,IAAIM,EAAW,MAAMR,EAAKC,EAAUC,CAAM,EAC1C,GAAIM,EAAS,SAAW,IAAK,CAC3B,IAAMC,EAAON,EAAaK,EAAS,QAAQ,IAAI,aAAa,CAAC,EAC7D,QAAQ,MAAM,oCAAqC,CAAE,OAAQA,EAAS,OAAQ,KAAAC,CAAK,CAAC,EACpF,MAAM,IAAI,QAAQC,GAAW,WAAWA,EAASD,CAAI,CAAC,EACtDD,EAAW,MAAMR,EAAKC,EAAUC,CAAM,CACxC,CACA,GAAI,CAACM,EAAS,GAAI,MAAM,IAAI,MAAM,yBAAyBA,EAAS,MAAM,EAAE,CAC9E,CACF,CC3BO,SAASG,EAAQC,EAAaC,EAAiBC,GAAUA,EAAc,CAC5E,MAAO,OAAMA,GAAU,CACrB,IAAMC,EAAW,MAAM,MAAMH,EAAK,CAChC,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAM,KAAK,UAAUC,EAAOC,CAAM,CAAC,EACnC,UAAW,GACX,YAAa,OACb,OAAQ,YAAY,QAAQ,GAAU,CACxC,CAAC,EACD,GAAI,CAACC,EAAS,GAAI,MAAM,IAAI,MAAM,oBAAoBA,EAAS,MAAM,EAAE,CACzE,CACF,CCCA,IAAMC,EAAQ,CAACC,EAAiBC,IAC9B,QAAQ,MAAM,gBAAgBD,CAAO,GAAIC,CAAI,EAGxC,SAASC,EAAKC,EAAmCC,EAAiC,CACvF,IAAMC,EAAOD,EAAI,YAAY,EAAE,MAAM,IAAK,CAAC,EAAE,CAAC,GAAK,GAC7CE,EAAQH,EAAQE,CAAI,EAC1B,GAAIC,EAAO,MAAO,CAACD,EAAMC,CAAK,EAC9B,GAAM,CAACC,CAAK,EAAI,OAAO,QAAQJ,CAAO,EACtC,GAAI,CAACI,EAAO,MAAM,IAAI,UAAU,mCAAmC,EACnE,OAAOA,CACT,CAGO,SAASC,EACdC,EACAC,EAAqB,WAAW,SACxB,CACR,IAAMC,EAAWD,EAAS,OAASA,EAAS,SAC5C,GAAI,OAAOD,GAAW,WAAY,CAChC,IAAMG,EAAYH,EAAOC,CAAQ,EAC7BG,EACJ,GAAI,CACFA,EAAS,IAAI,IAAID,CAAS,CAC5B,MAAQ,CACN,eAAQ,MAAM,wDAAyD,CAAE,MAAOA,CAAU,CAAC,EACpFD,CACT,CACA,OAAIE,EAAO,WAAa,SAAWA,EAAO,WAAa,UAAYA,EAAO,OAAS,IACjF,QAAQ,MAAM,wDAAyD,CAAE,MAAOD,CAAU,CAAC,EACpFD,IAETE,EAAO,KAAO,GACVA,EAAO,SAAW,MAAKA,EAAO,OAAS,IACpCA,EAAO,OAASA,EAAO,SAAWA,EAAO,OAClD,CACA,OAAQJ,EAAQ,CACd,IAAK,OAAQ,CACX,IAAMI,EAAS,IAAI,IAAIH,EAAS,IAAI,EACpC,OAAAG,EAAO,KAAO,GACVA,EAAO,SAAW,MAAKA,EAAO,OAAS,IACpCA,EAAO,OAASA,EAAO,SAAWA,EAAO,MAClD,CACA,IAAK,SACH,OAAOH,EAAS,OAElB,QACE,OAAOC,CAEX,CACF,CAGO,SAASG,EAAOX,EAAmC,CACxD,OAAO,SAAoBY,EAA8B,CACvD,GAAM,CACJ,SAAAC,EACA,UAAAC,EAAY,QACZ,OAAAC,EACA,aAAAC,EAAe,IACf,WAAAC,EAAa,IACb,QAAAC,EAAU,GACV,IAAAC,EAAM,MACR,EAAIP,EACJ,GAAI,CAACC,GAAY,CAACD,EAAQ,KAAM,MAAM,IAAI,UAAU,uCAAuC,EAC3F,IAAIQ,EACJ,GAAIR,EAAQ,KAAMQ,EAAOR,EAAQ,SAC5B,IAAIC,IAAa,OAAW,MAAM,IAAI,UAAU,uCAAuC,EACvFO,EAAON,IAAc,UAAYO,EAAQR,EAAUE,CAAM,EAAIO,EAAMT,CAAQ,EAChF,IAAMU,EAAW,SAAS,gBAAgB,KACpC,CAACC,EAAQC,CAAQ,EAAI1B,EAAKC,EAASY,EAAQ,SAAWW,GAAY,UAAU,SAAS,EACrFG,EAAKd,EAAQ,KAAOe,GAASC,EAAQD,EAAOF,EAAUD,EAAQN,CAAO,GACvEW,EAAO,KACPC,EAAS,GACPC,EAAQ,MAAOC,GAAwC,CAC3D,GAAI,CAACC,EAAUD,CAAK,EAAG,OACvB,GAAIE,EAAWF,EAAM,MAAM,EACzB,OAAOpC,EAAM,+BAAgC,CAAE,OAAQoC,EAAM,MAAO,CAAC,EACvE,IAAML,EAAQQ,EAAQ,aAAa,EAAGnB,CAAY,EAClD,GAAI,CAACW,EAAO,OAAO/B,EAAM,8BAA+B,CAAE,aAAAoB,CAAa,CAAC,EACxE,IAAMoB,EAAOP,EAAOZ,EAAa,KAAK,IAAI,EAC1C,GAAImB,EAAO,EAAG,OAAOxC,EAAM,6BAA8B,CAAE,KAAAwC,CAAK,CAAC,EACjE,GAAIN,EAAQ,OAAOlC,EAAM,mCAAoC,CAAE,UAAW+B,EAAM,SAAU,CAAC,EAC3FK,EAAM,eAAe,EACrBF,EAAS,GACT,GAAI,CACF,IAAMO,EAAe,MAAMX,EAAGC,CAAK,EACnC,GAAI,CAACU,EAAc,OAAOzC,EAAM,YAAa,CAAE,UAAW+B,EAAM,SAAU,CAAC,EAC3EE,EAAO,KAAK,IAAI,EAChB,IAAMS,EAAOC,EAAUF,EAAa,SAAS,KAAK,GAAK,EAAE,EACnDG,EAAO,CAAE,GAAGb,EAAO,KAAMJ,GAAYC,EAAQ,GAAIc,GAAQ,CAAE,QAASA,CAAK,CAAG,EAClF,MAAMlB,EAAK,CAAE,KAAM,OAAQ,IAAK,EAAG,IAAKf,EAAWc,EAAK,QAAQ,EAAG,KAAAqB,CAAK,CAAC,EACzE5C,EAAM,OAAQ,CAAE,UAAW+B,EAAM,UAAW,UAAAb,EAAW,QAAS,EAAQwB,CAAM,CAAC,CACjF,OAASG,EAAO,CACd7C,EAAM,WAAY,CAAE,MAAA6C,CAAM,CAAC,CAC7B,QAAE,CACAX,EAAS,EACX,CACF,EACMY,EAAYV,GAA4B,CAAQD,EAAMC,CAAK,GACjE,gBAAS,iBAAiB,UAAWU,CAAQ,EAC7C9C,EAAM,YAAa,CAAE,OAAA4B,EAAQ,UAAAV,EAAW,WAAAG,CAAW,CAAC,EAC7C,IAAM,CACX,SAAS,oBAAoB,UAAWyB,CAAQ,EAChD9C,EAAM,WAAY,CAAE,OAAA4B,CAAO,CAAC,CAC9B,CACF,CACF,CCtHO,IAAMmB,GAAaC,EAAO,CAAE,GAAAC,CAAG,CAAC","names":["es_default","EDITABLE","BLOCK","isTrigger","event","isEditable","target","MAX_TEXT_BYTES","clipBytes","text","max","encoder","clipped","squash","toDraft","before","selected","after","prefix","suffix","head","tail","renderedTextNodes","scope","nodes","walker","node","element","capture","selection","range","common","draft","beforeText","selectedText","from","to","afterText","hasStarted","CSS","element","tag","parent","text","node","confirm","draft","messages","lang","hasComment","host","root","sheet","dialog","form","context","label","comment","MAX_TEXT_BYTES","menu","send","resolve","post","endpoint","report","retryAfterMs","header","random","seconds","relay","response","wait","resolve","webhook","url","format","report","response","debug","message","data","pick","catalog","tag","base","found","first","resolveUrl","option","location","fallback","candidate","parsed","create","options","endpoint","transport","format","maxSelection","cooldownMs","comment","url","send","webhook","relay","pageLang","locale","messages","ui","draft","confirm","last","isOpen","onKey","event","isTrigger","isEditable","capture","wait","confirmation","note","clipBytes","body","error","listener","reportTypo","create","es_default"]}
|
|
1
|
+
{"version":3,"sources":["../locales/es.yaml","../src/capture.ts","../src/dialog.ts","../src/transport/relay.ts","../src/transport/webhook.ts","../src/core.ts","../src/entries/es.ts"],"sourcesContent":["export default {\"dir\":\"ltr\",\"title\":\"Avisar de una errata\",\"send\":\"Enviar\",\"cancel\":\"Cancelar\",\"comment\":\"Comentario (opcional)\",\"commentPlaceholder\":\"¿Qué debería decir?\"}","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Draft } from './types.js'\n\n/** Characters of context kept on each side of the selection. */\nexport const CONTEXT_CHARS = 64\n\nconst EDITABLE = 'input,textarea,select,[contenteditable]:not([contenteditable=\"false\"])'\nconst BLOCK =\n 'p,li,dd,dt,td,th,blockquote,pre,figcaption,h1,h2,h3,h4,h5,h6,article,section,main,body'\n\n/** Reports whether a key event is Ctrl+Enter, or Cmd+Enter on macOS. */\nexport function isTrigger(event: Pick<KeyboardEvent, 'ctrlKey' | 'metaKey' | 'key'>): boolean {\n return (event.ctrlKey || event.metaKey) && event.key === 'Enter'\n}\n\n/** Reports whether the event target is a field the reader is typing in. */\nexport function isEditable(target: EventTarget | null): boolean {\n const element = target as Partial<Element> | null\n return Boolean(element?.closest?.(EDITABLE))\n}\n\n/** Bytes report-relay keeps of one free-text field. */\nexport const MAX_TEXT_BYTES = 1024\n\n/** Cuts text to a UTF-8 byte budget on a code point boundary. */\nexport function clipBytes(text: string, max = MAX_TEXT_BYTES): string {\n const encoder = new TextEncoder()\n let clipped = text\n while (encoder.encode(clipped).length > max) clipped = [...clipped].slice(0, -1).join('')\n return clipped\n}\n\nconst squash = (text: string): string => text.replaceAll(/\\s+/g, ' ')\n\n/** Builds a draft from the text before, inside and after a selection, cut to whole words. */\nexport function toDraft(before: string, selected: string, after: string, max: number): Draft {\n const prefix = squash(before)\n const suffix = squash(after)\n const head = prefix.slice(-CONTEXT_CHARS)\n const tail = suffix.slice(0, CONTEXT_CHARS)\n return {\n selection: squash(selected).trim().slice(0, max),\n prefix: (prefix.length > CONTEXT_CHARS ? head.replace(/^\\S*\\s/, '') : head).trimStart(),\n suffix: (suffix.length > CONTEXT_CHARS ? tail.replace(/\\s\\S*$/, '') : tail).trimEnd()\n }\n}\n\n/** Collects the rendered text nodes of a subtree, skipping non-rendered and hidden ones. */\nfunction renderedTextNodes(scope: Node): Text[] {\n const nodes: Text[] = []\n const walker = document.createTreeWalker(scope, 5, {\n acceptNode(node) {\n if (node.nodeType === 3) return 1\n const element = node as Element\n return '|SCRIPT|STYLE|TEMPLATE|NOSCRIPT|'.includes('|' + element.tagName + '|') ||\n element.hasAttribute('hidden') ||\n element.getAttribute('aria-hidden') === 'true'\n ? 2\n : 3\n }\n })\n for (let node = walker.firstChild(); node; node = walker.nextNode()) {\n if (node.nodeType === 3) nodes.push(node as Text)\n }\n return nodes\n}\n\n/** Reads the current selection and its surrounding text from the page. */\nexport function capture(selection: Selection | null, max: number): Draft | undefined {\n if (!selection || selection.isCollapsed || selection.rangeCount === 0) return\n const range = selection.getRangeAt(0)\n const common = range.commonAncestorContainer\n const scope =\n (common instanceof Element ? common : common.parentElement)?.closest(BLOCK) ?? document.body\n\n if (typeof range.comparePoint !== 'function') {\n const before = document.createRange()\n before.selectNodeContents(scope)\n before.setEnd(range.startContainer, range.startOffset)\n const after = document.createRange()\n after.selectNodeContents(scope)\n after.setStart(range.endContainer, range.endOffset)\n const draft = toDraft(before.toString(), range.toString(), after.toString(), max)\n return draft.selection ? draft : undefined\n }\n\n const nodes = renderedTextNodes(scope)\n\n let beforeText = ''\n for (const node of nodes) {\n if (node === range.startContainer) {\n beforeText += node.data.slice(0, range.startOffset)\n break\n }\n if (range.comparePoint(node, node.data.length) < 0) beforeText += node.data\n else break\n }\n\n let selectedText = ''\n for (const node of nodes) {\n if (range.comparePoint(node, node.data.length) < 0) continue\n if (range.comparePoint(node, 0) > 0) break\n let from = 0\n let to = node.data.length\n if (node === range.startContainer) from = range.startOffset\n if (node === range.endContainer) to = range.endOffset\n if (to > from) selectedText += node.data.slice(from, to)\n }\n if (!selectedText) selectedText = range.toString()\n\n let afterText = ''\n let hasStarted = false\n for (const node of nodes) {\n if (hasStarted) {\n afterText += node.data\n } else if (node === range.endContainer) {\n afterText += node.data.slice(range.endOffset)\n hasStarted = true\n } else if (range.comparePoint(node, 0) > 0) {\n afterText += node.data\n hasStarted = true\n }\n }\n\n const draft = toDraft(beforeText, selectedText, afterText, max)\n return draft.selection ? draft : undefined\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport { MAX_TEXT_BYTES } from './capture.js'\nimport type { Confirmation, Draft, Messages } from './types.js'\n\nconst CSS = `dialog{max-inline-size:min(32rem,90vw);font:var(--report-typo-font,1rem/1.5 system-ui,sans-serif);\ncolor:var(--report-typo-color,CanvasText);background:var(--report-typo-background,Canvas);\nborder:var(--report-typo-border,1px solid GrayText);border-radius:var(--report-typo-radius,.5rem)}\np,label{display:block;margin:0 0 1rem}mark{padding:0 .1em;background:var(--report-typo-highlight,Mark)}\ntextarea{display:block;box-sizing:border-box;inline-size:100%;font:inherit}\nmenu{display:flex;gap:.5rem;justify-content:end;margin:0;padding:0}`\n\nconst element = <K extends keyof HTMLElementTagNameMap>(\n tag: K,\n parent: ParentNode,\n text = ''\n): HTMLElementTagNameMap[K] => {\n const node = document.createElement(tag)\n node.textContent = text\n parent.append(node)\n return node\n}\n\n/** Shows the draft in a modal dialog inside a closed shadow root and waits for Send or Cancel. */\nexport function confirm(\n draft: Draft,\n messages: Messages,\n lang: string,\n hasComment: boolean\n): Promise<Confirmation | undefined> {\n const host = document.createElement('report-typo')\n const root = host.attachShadow({ mode: 'closed' })\n const sheet = new CSSStyleSheet()\n sheet.replaceSync(CSS)\n root.adoptedStyleSheets = [sheet]\n const dialog = element('dialog', root)\n dialog.dir = messages.dir\n dialog.lang = lang\n const form = element('form', dialog)\n form.method = 'dialog'\n element('h2', form, messages.title)\n const context = element('p', form, draft.prefix)\n element('mark', context, draft.selection)\n context.append(draft.suffix)\n const label = hasComment ? element('label', form, messages.comment) : undefined\n const comment = label ? element('textarea', label) : undefined\n if (comment) {\n comment.placeholder = messages.commentPlaceholder\n comment.maxLength = MAX_TEXT_BYTES\n comment.rows = 3\n }\n const menu = element('menu', form)\n element('button', menu, messages.cancel).value = 'cancel'\n const send = element('button', menu, messages.send)\n send.value = 'send'\n send.autofocus = !comment\n document.body.append(host)\n return new Promise(resolve => {\n dialog.addEventListener('close', () => {\n host.remove()\n resolve(dialog.returnValue === 'send' ? { comment: comment?.value } : undefined)\n })\n dialog.showModal()\n comment?.focus()\n })\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Send, TypoReport } from '../types.js'\n\n/** Per-attempt deadline of one delivery. */\nexport const TIMEOUT_MS = 5000\n\nconst post = (endpoint: string, report: TypoReport): Promise<Response> =>\n fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/reports+json' },\n body: JSON.stringify([report]),\n keepalive: true,\n credentials: 'omit',\n mode: 'cors',\n signal: AbortSignal.timeout(TIMEOUT_MS)\n })\n\n/** Milliseconds to wait on a `Retry-After` header, with up to 50% jitter; 1 s when absent or a date. */\nexport function retryAfterMs(header: string | null, random = Math.random): number {\n const seconds = Number(header)\n return (Number.isFinite(seconds) && seconds > 0 ? seconds : 1) * 1000 * (1 + random() / 2)\n}\n\n/** Sends reports to a report-relay intake, retrying once on 429. */\nexport function relay(endpoint: string): Send {\n return async report => {\n let response = await post(endpoint, report)\n if (response.status === 429) {\n const wait = retryAfterMs(response.headers.get('Retry-After'))\n console.debug('report-typo: relay busy, retrying', { status: response.status, wait })\n await new Promise(resolve => setTimeout(resolve, wait))\n response = await post(endpoint, report)\n }\n if (!response.ok) throw new Error(`report-relay answered ${response.status}`)\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Send, TypoReport } from '../types.js'\nimport { TIMEOUT_MS } from './relay.js'\n\n/** Turns a report into the JSON a webhook expects. */\nexport type Format = (report: TypoReport) => unknown\n\n/** Sends reports as a plain JSON POST, shaped by `format` when given. */\nexport function webhook(url: string, format: Format = report => report): Send {\n return async report => {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(format(report)),\n keepalive: true,\n credentials: 'omit',\n signal: AbortSignal.timeout(TIMEOUT_MS)\n })\n if (!response.ok) throw new Error(`webhook answered ${response.status}`)\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport { capture, clipBytes, isEditable, isTrigger } from './capture.js'\nimport { confirm } from './dialog.js'\nimport { relay } from './transport/relay.js'\nimport { webhook, type Format } from './transport/webhook.js'\nimport type { Messages, Send, Ui } from './types.js'\n\n/** Options of `reportTypo`; `endpoint` or `send` is required. */\nexport interface Options {\n endpoint?: string\n transport?: 'relay' | 'webhook'\n format?: Format\n send?: Send\n locale?: string\n maxSelection?: number\n cooldownMs?: number\n comment?: boolean\n ui?: Ui\n url?: 'full' | 'path' | 'origin' | ((location: Location) => string)\n}\n\nconst debug = (message: string, data: unknown): void =>\n console.debug(`report-typo: ${message}`, data)\n\n/** Picks the catalog entry for a language tag by its base subtag, falling back to the first entry. */\nexport function pick(catalog: Record<string, Messages>, tag: string): [string, Messages] {\n const base = tag.toLowerCase().split('-', 1)[0] ?? ''\n const found = catalog[base]\n if (found) return [base, found]\n const [first] = Object.entries(catalog)\n if (!first) throw new TypeError('report-typo: empty locale catalog')\n return first\n}\n\n/** Resolves the page URL to send, per the `url` option, dropping the fragment. */\nexport function resolveUrl(\n option: Options['url'],\n location: Location = globalThis.location\n): string {\n const fallback = location.origin + location.pathname\n if (typeof option === 'function') {\n const candidate = option(location)\n let parsed: URL\n try {\n parsed = new URL(candidate)\n } catch {\n console.debug('report-typo: invalid url option, falling back to path', { value: candidate })\n return fallback\n }\n if ((parsed.protocol !== 'http:' && parsed.protocol !== 'https:') || parsed.host === '') {\n console.debug('report-typo: invalid url option, falling back to path', { value: candidate })\n return fallback\n }\n parsed.hash = ''\n if (parsed.search === '?') parsed.search = ''\n return parsed.origin + parsed.pathname + parsed.search\n }\n switch (option) {\n case 'full': {\n const parsed = new URL(location.href)\n parsed.hash = ''\n if (parsed.search === '?') parsed.search = ''\n return parsed.origin + parsed.pathname + parsed.search\n }\n case 'origin': {\n return location.origin\n }\n default: {\n return fallback\n }\n }\n}\n\n/** Builds `reportTypo` over a locale catalog, so each bundle ships only the locales it imports. */\nexport function create(catalog: Record<string, Messages>) {\n return function reportTypo(options: Options): () => void {\n const {\n endpoint,\n transport = 'relay',\n format,\n maxSelection = 256,\n cooldownMs = 2000,\n comment = false,\n url = 'path'\n } = options\n if (!endpoint && !options.send) throw new TypeError('report-typo: set `endpoint` or `send`')\n let send: Send\n if (options.send) send = options.send\n else if (endpoint === undefined) throw new TypeError('report-typo: set `endpoint` or `send`')\n else send = transport === 'webhook' ? webhook(endpoint, format) : relay(endpoint)\n const pageLang = document.documentElement.lang\n const [locale, messages] = pick(catalog, options.locale ?? (pageLang || navigator.language))\n const ui = options.ui ?? (draft => confirm(draft, messages, locale, comment))\n let last = -Infinity\n let isOpen = false\n const onKey = async (event: KeyboardEvent): Promise<void> => {\n if (!isTrigger(event)) return\n if (isEditable(event.target))\n return debug('ignored in an editable field', { target: event.target })\n const draft = capture(getSelection(), maxSelection)\n if (!draft) return debug('ignored without a selection', { maxSelection })\n const wait = last + cooldownMs - Date.now()\n if (wait > 0) return debug('ignored while cooling down', { wait })\n if (isOpen) return debug('ignored while the dialog is open', { selection: draft.selection })\n event.preventDefault()\n isOpen = true\n try {\n const confirmation = await ui(draft)\n if (!confirmation) return debug('cancelled', { selection: draft.selection })\n last = Date.now()\n const note = clipBytes(confirmation.comment?.trim() ?? '')\n const body = { ...draft, lang: pageLang || locale, ...(note && { comment: note }) }\n await send({ type: 'typo', age: 0, url: resolveUrl(url, location), body })\n debug('sent', { selection: draft.selection, transport, comment: Boolean(note) })\n } catch (error) {\n debug('not sent', { error })\n } finally {\n isOpen = false\n }\n }\n const listener = (event: KeyboardEvent): void => void onKey(event)\n document.addEventListener('keydown', listener)\n debug('listening', { locale, transport, cooldownMs })\n return () => {\n document.removeEventListener('keydown', listener)\n debug('disposed', { locale })\n }\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport es from '../../locales/es.yaml'\nimport { create } from '../core.js'\n\nexport type { Options } from '../core.js'\nexport type { Confirmation, Draft, Messages, Send, TypoBody, TypoReport, Ui } from '../types.js'\nexport { relay } from '../transport/relay.js'\nexport { webhook, type Format } from '../transport/webhook.js'\n\n/** Starts listening for Ctrl+Enter and returns the function that stops it. */\nexport const reportTypo = create({ es })\n"],"mappings":"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","names":["es_default","EDITABLE","BLOCK","isTrigger","event","isEditable","target","MAX_TEXT_BYTES","clipBytes","text","max","encoder","clipped","squash","toDraft","before","selected","after","prefix","suffix","head","tail","renderedTextNodes","scope","nodes","walker","node","element","capture","selection","range","common","draft","beforeText","selectedText","from","to","afterText","hasStarted","CSS","element","tag","parent","text","node","confirm","draft","messages","lang","hasComment","host","root","sheet","dialog","form","context","label","comment","MAX_TEXT_BYTES","menu","send","resolve","post","endpoint","report","retryAfterMs","header","random","seconds","relay","response","wait","resolve","webhook","url","format","report","response","debug","message","data","pick","catalog","tag","base","found","first","resolveUrl","option","location","fallback","candidate","parsed","create","options","endpoint","transport","format","maxSelection","cooldownMs","comment","url","send","webhook","relay","pageLang","locale","messages","ui","draft","confirm","last","isOpen","onKey","event","isTrigger","isEditable","capture","wait","confirmation","note","clipBytes","body","error","listener","reportTypo","create","es_default"]}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../locales/en.yaml","../locales/es.yaml","../locales/uk.yaml","../src/capture.ts","../src/dialog.ts","../src/transport/relay.ts","../src/transport/webhook.ts","../src/core.ts","../src/index.ts"],"sourcesContent":["export default {\"dir\":\"ltr\",\"title\":\"Report a typo\",\"send\":\"Send\",\"cancel\":\"Cancel\",\"comment\":\"Comment (optional)\",\"commentPlaceholder\":\"What should it say?\"}","export default {\"dir\":\"ltr\",\"title\":\"Avisar de una errata\",\"send\":\"Enviar\",\"cancel\":\"Cancelar\",\"comment\":\"Comentario (opcional)\",\"commentPlaceholder\":\"¿Qué debería decir?\"}","export default {\"dir\":\"ltr\",\"title\":\"Повідомити про одруківку\",\"send\":\"Надіслати\",\"cancel\":\"Скасувати\",\"comment\":\"Коментар (необов’язково)\",\"commentPlaceholder\":\"Як має бути?\"}","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Draft } from './types.js'\n\n/** Characters of context kept on each side of the selection. */\nexport const CONTEXT_CHARS = 64\n\nconst EDITABLE = 'input,textarea,select,[contenteditable]:not([contenteditable=\"false\"])'\nconst BLOCK =\n 'p,li,dd,dt,td,th,blockquote,pre,figcaption,h1,h2,h3,h4,h5,h6,article,section,main,body'\n\n/** Reports whether a key event is Ctrl+Enter, or Cmd+Enter on macOS. */\nexport function isTrigger(event: Pick<KeyboardEvent, 'ctrlKey' | 'metaKey' | 'key'>): boolean {\n return (event.ctrlKey || event.metaKey) && event.key === 'Enter'\n}\n\n/** Reports whether the event target is a field the reader is typing in. */\nexport function isEditable(target: EventTarget | null): boolean {\n const element = target as Partial<Element> | null\n return Boolean(element?.closest?.(EDITABLE))\n}\n\n/** Bytes report-relay keeps of one free-text field. */\nexport const MAX_TEXT_BYTES = 1024\n\n/** Cuts text to a UTF-8 byte budget on a code point boundary. */\nexport function clipBytes(text: string, max = MAX_TEXT_BYTES): string {\n const encoder = new TextEncoder()\n let clipped = text\n while (encoder.encode(clipped).length > max) clipped = [...clipped].slice(0, -1).join('')\n return clipped\n}\n\nconst squash = (text: string): string => text.replaceAll(/\\s+/g, ' ')\n\n/** Builds a draft from the text before, inside and after a selection, cut to whole words. */\nexport function toDraft(before: string, selected: string, after: string, max: number): Draft {\n const prefix = squash(before)\n const suffix = squash(after)\n const head = prefix.slice(-CONTEXT_CHARS)\n const tail = suffix.slice(0, CONTEXT_CHARS)\n return {\n selection: squash(selected).trim().slice(0, max),\n prefix: (prefix.length > CONTEXT_CHARS ? head.replace(/^\\S*\\s/, '') : head).trimStart(),\n suffix: (suffix.length > CONTEXT_CHARS ? tail.replace(/\\s\\S*$/, '') : tail).trimEnd()\n }\n}\n\n/** Collects the rendered text nodes of a subtree, skipping non-rendered and hidden ones. */\nfunction renderedTextNodes(scope: Node): Text[] {\n const nodes: Text[] = []\n const walker = document.createTreeWalker(scope, 5, {\n acceptNode(node) {\n if (node.nodeType === 3) return 1\n const element = node as Element\n return '|SCRIPT|STYLE|TEMPLATE|NOSCRIPT|'.includes('|' + element.tagName + '|') ||\n element.hasAttribute('hidden') ||\n element.getAttribute('aria-hidden') === 'true'\n ? 2\n : 3\n }\n })\n for (let node = walker.firstChild(); node; node = walker.nextNode()) {\n if (node.nodeType === 3) nodes.push(node as Text)\n }\n return nodes\n}\n\n/** Reads the current selection and its surrounding text from the page. */\nexport function capture(selection: Selection | null, max: number): Draft | undefined {\n if (!selection || selection.isCollapsed || selection.rangeCount === 0) return\n const range = selection.getRangeAt(0)\n const common = range.commonAncestorContainer\n const scope =\n (common instanceof Element ? common : common.parentElement)?.closest(BLOCK) ?? document.body\n\n if (typeof range.comparePoint !== 'function') {\n const before = document.createRange()\n before.selectNodeContents(scope)\n before.setEnd(range.startContainer, range.startOffset)\n const after = document.createRange()\n after.selectNodeContents(scope)\n after.setStart(range.endContainer, range.endOffset)\n const draft = toDraft(before.toString(), range.toString(), after.toString(), max)\n return draft.selection ? draft : undefined\n }\n\n const nodes = renderedTextNodes(scope)\n\n let beforeText = ''\n for (const node of nodes) {\n if (node === range.startContainer) {\n beforeText += node.data.slice(0, range.startOffset)\n break\n }\n if (range.comparePoint(node, node.data.length) < 0) beforeText += node.data\n else break\n }\n\n let selectedText = ''\n for (const node of nodes) {\n if (range.comparePoint(node, node.data.length) < 0) continue\n if (range.comparePoint(node, 0) > 0) break\n let from = 0\n let to = node.data.length\n if (node === range.startContainer) from = range.startOffset\n if (node === range.endContainer) to = range.endOffset\n if (to > from) selectedText += node.data.slice(from, to)\n }\n if (!selectedText) selectedText = range.toString()\n\n let afterText = ''\n let hasStarted = false\n for (const node of nodes) {\n if (hasStarted) {\n afterText += node.data\n } else if (node === range.endContainer) {\n afterText += node.data.slice(range.endOffset)\n hasStarted = true\n } else if (range.comparePoint(node, 0) > 0) {\n afterText += node.data\n hasStarted = true\n }\n }\n\n const draft = toDraft(beforeText, selectedText, afterText, max)\n return draft.selection ? draft : undefined\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport { MAX_TEXT_BYTES } from './capture.js'\nimport type { Confirmation, Draft, Messages } from './types.js'\n\nconst CSS = `dialog{max-inline-size:min(32rem,90vw);font:var(--report-typo-font,1rem/1.5 system-ui,sans-serif);\ncolor:var(--report-typo-color,CanvasText);background:var(--report-typo-background,Canvas);\nborder:var(--report-typo-border,1px solid GrayText);border-radius:var(--report-typo-radius,.5rem)}\np,label{display:block;margin:0 0 1rem}mark{padding:0 .1em;background:var(--report-typo-highlight,Mark)}\ntextarea{display:block;box-sizing:border-box;inline-size:100%;font:inherit}\nmenu{display:flex;gap:.5rem;justify-content:end;margin:0;padding:0}`\n\nconst element = <K extends keyof HTMLElementTagNameMap>(\n tag: K,\n parent: ParentNode,\n text = ''\n): HTMLElementTagNameMap[K] => {\n const node = document.createElement(tag)\n node.textContent = text\n parent.append(node)\n return node\n}\n\n/** Shows the draft in a modal dialog inside a closed shadow root and waits for Send or Cancel. */\nexport function confirm(\n draft: Draft,\n messages: Messages,\n lang: string,\n hasComment: boolean\n): Promise<Confirmation | undefined> {\n const host = document.createElement('report-typo')\n const root = host.attachShadow({ mode: 'closed' })\n const sheet = new CSSStyleSheet()\n sheet.replaceSync(CSS)\n root.adoptedStyleSheets = [sheet]\n const dialog = element('dialog', root)\n dialog.dir = messages.dir\n dialog.lang = lang\n const form = element('form', dialog)\n form.method = 'dialog'\n element('h2', form, messages.title)\n const context = element('p', form, draft.prefix)\n element('mark', context, draft.selection)\n context.append(draft.suffix)\n const label = hasComment ? element('label', form, messages.comment) : undefined\n const comment = label ? element('textarea', label) : undefined\n if (comment) {\n comment.placeholder = messages.commentPlaceholder\n comment.maxLength = MAX_TEXT_BYTES\n comment.rows = 3\n }\n const menu = element('menu', form)\n element('button', menu, messages.cancel).value = 'cancel'\n const send = element('button', menu, messages.send)\n send.value = 'send'\n send.autofocus = !comment\n document.body.append(host)\n return new Promise(resolve => {\n dialog.addEventListener('close', () => {\n host.remove()\n resolve(dialog.returnValue === 'send' ? { comment: comment?.value } : undefined)\n })\n dialog.showModal()\n comment?.focus()\n })\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Send, TypoReport } from '../types.js'\n\n/** Per-attempt deadline of one delivery. */\nexport const TIMEOUT_MS = 5000\n\nconst post = (endpoint: string, report: TypoReport): Promise<Response> =>\n fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/reports+json' },\n body: JSON.stringify([report]),\n keepalive: true,\n credentials: 'omit',\n mode: 'cors',\n signal: AbortSignal.timeout(TIMEOUT_MS)\n })\n\n/** Milliseconds to wait on a `Retry-After` header, with up to 50% jitter; 1 s when absent or a date. */\nexport function retryAfterMs(header: string | null, random = Math.random): number {\n const seconds = Number(header)\n return (Number.isFinite(seconds) && seconds > 0 ? seconds : 1) * 1000 * (1 + random() / 2)\n}\n\n/** Sends reports to a report-relay intake, retrying once on 429. */\nexport function relay(endpoint: string): Send {\n return async report => {\n let response = await post(endpoint, report)\n if (response.status === 429) {\n const wait = retryAfterMs(response.headers.get('Retry-After'))\n console.debug('report-typo: relay busy, retrying', { status: response.status, wait })\n await new Promise(resolve => setTimeout(resolve, wait))\n response = await post(endpoint, report)\n }\n if (!response.ok) throw new Error(`report-relay answered ${response.status}`)\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Send, TypoReport } from '../types.js'\nimport { TIMEOUT_MS } from './relay.js'\n\n/** Turns a report into the JSON a webhook expects. */\nexport type Format = (report: TypoReport) => unknown\n\n/** Sends reports as a plain JSON POST, shaped by `format` when given. */\nexport function webhook(url: string, format: Format = report => report): Send {\n return async report => {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(format(report)),\n keepalive: true,\n credentials: 'omit',\n signal: AbortSignal.timeout(TIMEOUT_MS)\n })\n if (!response.ok) throw new Error(`webhook answered ${response.status}`)\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport { capture, clipBytes, isEditable, isTrigger } from './capture.js'\nimport { confirm } from './dialog.js'\nimport { relay } from './transport/relay.js'\nimport { webhook, type Format } from './transport/webhook.js'\nimport type { Messages, Send, Ui } from './types.js'\n\n/** Options of `reportTypo`; `endpoint` or `send` is required. */\nexport interface Options {\n endpoint?: string\n transport?: 'relay' | 'webhook'\n format?: Format\n send?: Send\n locale?: string\n maxSelection?: number\n cooldownMs?: number\n comment?: boolean\n ui?: Ui\n url?: 'full' | 'path' | 'origin' | ((location: Location) => string)\n}\n\nconst debug = (message: string, data: unknown): void =>\n console.debug(`report-typo: ${message}`, data)\n\n/** Picks the catalog entry for a language tag by its base subtag, falling back to the first entry. */\nexport function pick(catalog: Record<string, Messages>, tag: string): [string, Messages] {\n const base = tag.toLowerCase().split('-', 1)[0] ?? ''\n const found = catalog[base]\n if (found) return [base, found]\n const [first] = Object.entries(catalog)\n if (!first) throw new TypeError('report-typo: empty locale catalog')\n return first\n}\n\n/** Resolves the page URL to send, per the `url` option, dropping the fragment. */\nexport function resolveUrl(\n option: Options['url'],\n location: Location = globalThis.location\n): string {\n const fallback = location.origin + location.pathname\n if (typeof option === 'function') {\n const candidate = option(location)\n let parsed: URL\n try {\n parsed = new URL(candidate)\n } catch {\n console.debug('report-typo: invalid url option, falling back to path', { value: candidate })\n return fallback\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:' || parsed.host === '') {\n console.debug('report-typo: invalid url option, falling back to path', { value: candidate })\n return fallback\n }\n parsed.hash = ''\n if (parsed.search === '?') parsed.search = ''\n return parsed.origin + parsed.pathname + parsed.search\n }\n switch (option) {\n case 'full': {\n const parsed = new URL(location.href)\n parsed.hash = ''\n if (parsed.search === '?') parsed.search = ''\n return parsed.origin + parsed.pathname + parsed.search\n }\n case 'origin': {\n return location.origin\n }\n default: {\n return fallback\n }\n }\n}\n\n/** Builds `reportTypo` over a locale catalog, so each bundle ships only the locales it imports. */\nexport function create(catalog: Record<string, Messages>) {\n return function reportTypo(options: Options): () => void {\n const {\n endpoint,\n transport = 'relay',\n format,\n maxSelection = 256,\n cooldownMs = 2000,\n comment = false,\n url = 'path'\n } = options\n if (!endpoint && !options.send) throw new TypeError('report-typo: set `endpoint` or `send`')\n let send: Send\n if (options.send) send = options.send\n else if (endpoint === undefined) throw new TypeError('report-typo: set `endpoint` or `send`')\n else send = transport === 'webhook' ? webhook(endpoint, format) : relay(endpoint)\n const pageLang = document.documentElement.lang\n const [locale, messages] = pick(catalog, options.locale ?? (pageLang || navigator.language))\n const ui = options.ui ?? (draft => confirm(draft, messages, locale, comment))\n let last = -Infinity\n let isOpen = false\n const onKey = async (event: KeyboardEvent): Promise<void> => {\n if (!isTrigger(event)) return\n if (isEditable(event.target))\n return debug('ignored in an editable field', { target: event.target })\n const draft = capture(getSelection(), maxSelection)\n if (!draft) return debug('ignored without a selection', { maxSelection })\n const wait = last + cooldownMs - Date.now()\n if (wait > 0) return debug('ignored while cooling down', { wait })\n if (isOpen) return debug('ignored while the dialog is open', { selection: draft.selection })\n event.preventDefault()\n isOpen = true\n try {\n const confirmation = await ui(draft)\n if (!confirmation) return debug('cancelled', { selection: draft.selection })\n last = Date.now()\n const note = clipBytes(confirmation.comment?.trim() ?? '')\n const body = { ...draft, lang: pageLang || locale, ...(note && { comment: note }) }\n await send({ type: 'typo', age: 0, url: resolveUrl(url, location), body })\n debug('sent', { selection: draft.selection, transport, comment: Boolean(note) })\n } catch (error) {\n debug('not sent', { error })\n } finally {\n isOpen = false\n }\n }\n const listener = (event: KeyboardEvent): void => void onKey(event)\n document.addEventListener('keydown', listener)\n debug('listening', { locale, transport, cooldownMs })\n return () => {\n document.removeEventListener('keydown', listener)\n debug('disposed', { locale })\n }\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport en from '../locales/en.yaml'\nimport es from '../locales/es.yaml'\nimport uk from '../locales/uk.yaml'\nimport { create } from './core.js'\n\nexport type { Options } from './core.js'\nexport type { Confirmation, Draft, Messages, Send, TypoBody, TypoReport, Ui } from './types.js'\nexport { relay } from './transport/relay.js'\nexport { webhook, type Format } from './transport/webhook.js'\n\n/** Starts listening for Ctrl+Enter and returns the function that stops it. */\nexport const reportTypo = create({ en, es, uk })\n"],"mappings":"AAAA,IAAOA,EAAQ,CAAC,IAAM,MAAM,MAAQ,gBAAgB,KAAO,OAAO,OAAS,SAAS,QAAU,qBAAqB,mBAAqB,qBAAqB,ECA7J,IAAOC,EAAQ,CAAC,IAAM,MAAM,MAAQ,uBAAuB,KAAO,SAAS,OAAS,WAAW,QAAU,wBAAwB,mBAAqB,8BAAqB,ECA3K,IAAOC,EAAQ,CAAC,IAAM,MAAM,MAAQ,yIAA2B,KAAO,yDAAY,OAAS,yDAAY,QAAU,oIAA2B,mBAAqB,2DAAc,ECS/K,IAAMC,EAAW,yEACXC,EACJ,yFAGK,SAASC,EAAUC,EAAoE,CAC5F,OAAQA,EAAM,SAAWA,EAAM,UAAYA,EAAM,MAAQ,OAC3D,CAGO,SAASC,EAAWC,EAAqC,CAE9D,MAAO,EADSA,GACQ,UAAUL,CAAQ,CAC5C,CAGO,IAAMM,EAAiB,KAGvB,SAASC,EAAUC,EAAcC,EAAMH,EAAwB,CACpE,IAAMI,EAAU,IAAI,YAChBC,EAAUH,EACd,KAAOE,EAAQ,OAAOC,CAAO,EAAE,OAASF,GAAKE,EAAU,CAAC,GAAGA,CAAO,EAAE,MAAM,EAAG,EAAE,EAAE,KAAK,EAAE,EACxF,OAAOA,CACT,CAEA,IAAMC,EAAUJ,GAAyBA,EAAK,WAAW,OAAQ,GAAG,EAG7D,SAASK,EAAQC,EAAgBC,EAAkBC,EAAeP,EAAoB,CAC3F,IAAMQ,EAASL,EAAOE,CAAM,EACtBI,EAASN,EAAOI,CAAK,EACrBG,EAAOF,EAAO,MAAM,GAAc,EAClCG,EAAOF,EAAO,MAAM,EAAG,EAAa,EAC1C,MAAO,CACL,UAAWN,EAAOG,CAAQ,EAAE,KAAK,EAAE,MAAM,EAAGN,CAAG,EAC/C,QAASQ,EAAO,OAAS,GAAgBE,EAAK,QAAQ,SAAU,EAAE,EAAIA,GAAM,UAAU,EACtF,QAASD,EAAO,OAAS,GAAgBE,EAAK,QAAQ,SAAU,EAAE,EAAIA,GAAM,QAAQ,CACtF,CACF,CAGA,SAASC,EAAkBC,EAAqB,CAC9C,IAAMC,EAAgB,CAAC,EACjBC,EAAS,SAAS,iBAAiBF,EAAO,EAAG,CACjD,WAAWG,EAAM,CACf,GAAIA,EAAK,WAAa,EAAG,MAAO,GAChC,IAAMC,EAAUD,EAChB,MAAO,mCAAmC,SAAS,IAAMC,EAAQ,QAAU,GAAG,GAC5EA,EAAQ,aAAa,QAAQ,GAC7BA,EAAQ,aAAa,aAAa,IAAM,OACtC,EACA,CACN,CACF,CAAC,EACD,QAASD,EAAOD,EAAO,WAAW,EAAGC,EAAMA,EAAOD,EAAO,SAAS,EAC5DC,EAAK,WAAa,GAAGF,EAAM,KAAKE,CAAY,EAElD,OAAOF,CACT,CAGO,SAASI,EAAQC,EAA6BnB,EAAgC,CACnF,GAAI,CAACmB,GAAaA,EAAU,aAAeA,EAAU,aAAe,EAAG,OACvE,IAAMC,EAAQD,EAAU,WAAW,CAAC,EAC9BE,EAASD,EAAM,wBACfP,GACHQ,aAAkB,QAAUA,EAASA,EAAO,gBAAgB,QAAQ7B,CAAK,GAAK,SAAS,KAE1F,GAAI,OAAO4B,EAAM,cAAiB,WAAY,CAC5C,IAAMf,EAAS,SAAS,YAAY,EACpCA,EAAO,mBAAmBQ,CAAK,EAC/BR,EAAO,OAAOe,EAAM,eAAgBA,EAAM,WAAW,EACrD,IAAMb,EAAQ,SAAS,YAAY,EACnCA,EAAM,mBAAmBM,CAAK,EAC9BN,EAAM,SAASa,EAAM,aAAcA,EAAM,SAAS,EAClD,IAAME,EAAQlB,EAAQC,EAAO,SAAS,EAAGe,EAAM,SAAS,EAAGb,EAAM,SAAS,EAAGP,CAAG,EAChF,OAAOsB,EAAM,UAAYA,EAAQ,MACnC,CAEA,IAAMR,EAAQF,EAAkBC,CAAK,EAEjCU,EAAa,GACjB,QAAWP,KAAQF,EAAO,CACxB,GAAIE,IAASI,EAAM,eAAgB,CACjCG,GAAcP,EAAK,KAAK,MAAM,EAAGI,EAAM,WAAW,EAClD,KACF,CACA,GAAIA,EAAM,aAAaJ,EAAMA,EAAK,KAAK,MAAM,EAAI,EAAGO,GAAcP,EAAK,SAClE,MACP,CAEA,IAAIQ,EAAe,GACnB,QAAWR,KAAQF,EAAO,CACxB,GAAIM,EAAM,aAAaJ,EAAMA,EAAK,KAAK,MAAM,EAAI,EAAG,SACpD,GAAII,EAAM,aAAaJ,EAAM,CAAC,EAAI,EAAG,MACrC,IAAIS,EAAO,EACPC,EAAKV,EAAK,KAAK,OACfA,IAASI,EAAM,iBAAgBK,EAAOL,EAAM,aAC5CJ,IAASI,EAAM,eAAcM,EAAKN,EAAM,WACxCM,EAAKD,IAAMD,GAAgBR,EAAK,KAAK,MAAMS,EAAMC,CAAE,EACzD,CACKF,IAAcA,EAAeJ,EAAM,SAAS,GAEjD,IAAIO,EAAY,GACZC,EAAa,GACjB,QAAWZ,KAAQF,EACbc,EACFD,GAAaX,EAAK,KACTA,IAASI,EAAM,cACxBO,GAAaX,EAAK,KAAK,MAAMI,EAAM,SAAS,EAC5CQ,EAAa,IACJR,EAAM,aAAaJ,EAAM,CAAC,EAAI,IACvCW,GAAaX,EAAK,KAClBY,EAAa,IAIjB,IAAMN,EAAQlB,EAAQmB,EAAYC,EAAcG,EAAW3B,CAAG,EAC9D,OAAOsB,EAAM,UAAYA,EAAQ,MACnC,CC1HA,IAAMO,EAAM;AAAA;AAAA;AAAA;AAAA;AAAA,qEAONC,EAAU,CACdC,EACAC,EACAC,EAAO,KACsB,CAC7B,IAAMC,EAAO,SAAS,cAAcH,CAAG,EACvC,OAAAG,EAAK,YAAcD,EACnBD,EAAO,OAAOE,CAAI,EACXA,CACT,EAGO,SAASC,EACdC,EACAC,EACAC,EACAC,EACmC,CACnC,IAAMC,EAAO,SAAS,cAAc,aAAa,EAC3CC,EAAOD,EAAK,aAAa,CAAE,KAAM,QAAS,CAAC,EAC3CE,EAAQ,IAAI,cAClBA,EAAM,YAAYb,CAAG,EACrBY,EAAK,mBAAqB,CAACC,CAAK,EAChC,IAAMC,EAASb,EAAQ,SAAUW,CAAI,EACrCE,EAAO,IAAMN,EAAS,IACtBM,EAAO,KAAOL,EACd,IAAMM,EAAOd,EAAQ,OAAQa,CAAM,EACnCC,EAAK,OAAS,SACdd,EAAQ,KAAMc,EAAMP,EAAS,KAAK,EAClC,IAAMQ,EAAUf,EAAQ,IAAKc,EAAMR,EAAM,MAAM,EAC/CN,EAAQ,OAAQe,EAAST,EAAM,SAAS,EACxCS,EAAQ,OAAOT,EAAM,MAAM,EAC3B,IAAMU,EAAQP,EAAaT,EAAQ,QAASc,EAAMP,EAAS,OAAO,EAAI,OAChEU,EAAUD,EAAQhB,EAAQ,WAAYgB,CAAK,EAAI,OACjDC,IACFA,EAAQ,YAAcV,EAAS,mBAC/BU,EAAQ,UAAYC,EACpBD,EAAQ,KAAO,GAEjB,IAAME,EAAOnB,EAAQ,OAAQc,CAAI,EACjCd,EAAQ,SAAUmB,EAAMZ,EAAS,MAAM,EAAE,MAAQ,SACjD,IAAMa,EAAOpB,EAAQ,SAAUmB,EAAMZ,EAAS,IAAI,EAClD,OAAAa,EAAK,MAAQ,OACbA,EAAK,UAAY,CAACH,EAClB,SAAS,KAAK,OAAOP,CAAI,EAClB,IAAI,QAAQW,GAAW,CAC5BR,EAAO,iBAAiB,QAAS,IAAM,CACrCH,EAAK,OAAO,EACZW,EAAQR,EAAO,cAAgB,OAAS,CAAE,QAASI,GAAS,KAAM,EAAI,MAAS,CACjF,CAAC,EACDJ,EAAO,UAAU,EACjBI,GAAS,MAAM,CACjB,CAAC,CACH,CC1DA,IAAMK,EAAO,CAACC,EAAkBC,IAC9B,MAAMD,EAAU,CACd,OAAQ,OACR,QAAS,CAAE,eAAgB,0BAA2B,EACtD,KAAM,KAAK,UAAU,CAACC,CAAM,CAAC,EAC7B,UAAW,GACX,YAAa,OACb,KAAM,OACN,OAAQ,YAAY,QAAQ,GAAU,CACxC,CAAC,EAGI,SAASC,EAAaC,EAAuBC,EAAS,KAAK,OAAgB,CAChF,IAAMC,EAAU,OAAOF,CAAM,EAC7B,OAAQ,OAAO,SAASE,CAAO,GAAKA,EAAU,EAAIA,EAAU,GAAK,KAAQ,EAAID,EAAO,EAAI,EAC1F,CAGO,SAASE,EAAMN,EAAwB,CAC5C,MAAO,OAAMC,GAAU,CACrB,IAAIM,EAAW,MAAMR,EAAKC,EAAUC,CAAM,EAC1C,GAAIM,EAAS,SAAW,IAAK,CAC3B,IAAMC,EAAON,EAAaK,EAAS,QAAQ,IAAI,aAAa,CAAC,EAC7D,QAAQ,MAAM,oCAAqC,CAAE,OAAQA,EAAS,OAAQ,KAAAC,CAAK,CAAC,EACpF,MAAM,IAAI,QAAQC,GAAW,WAAWA,EAASD,CAAI,CAAC,EACtDD,EAAW,MAAMR,EAAKC,EAAUC,CAAM,CACxC,CACA,GAAI,CAACM,EAAS,GAAI,MAAM,IAAI,MAAM,yBAAyBA,EAAS,MAAM,EAAE,CAC9E,CACF,CC3BO,SAASG,EAAQC,EAAaC,EAAiBC,GAAUA,EAAc,CAC5E,MAAO,OAAMA,GAAU,CACrB,IAAMC,EAAW,MAAM,MAAMH,EAAK,CAChC,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAM,KAAK,UAAUC,EAAOC,CAAM,CAAC,EACnC,UAAW,GACX,YAAa,OACb,OAAQ,YAAY,QAAQ,GAAU,CACxC,CAAC,EACD,GAAI,CAACC,EAAS,GAAI,MAAM,IAAI,MAAM,oBAAoBA,EAAS,MAAM,EAAE,CACzE,CACF,CCCA,IAAMC,EAAQ,CAACC,EAAiBC,IAC9B,QAAQ,MAAM,gBAAgBD,CAAO,GAAIC,CAAI,EAGxC,SAASC,EAAKC,EAAmCC,EAAiC,CACvF,IAAMC,EAAOD,EAAI,YAAY,EAAE,MAAM,IAAK,CAAC,EAAE,CAAC,GAAK,GAC7CE,EAAQH,EAAQE,CAAI,EAC1B,GAAIC,EAAO,MAAO,CAACD,EAAMC,CAAK,EAC9B,GAAM,CAACC,CAAK,EAAI,OAAO,QAAQJ,CAAO,EACtC,GAAI,CAACI,EAAO,MAAM,IAAI,UAAU,mCAAmC,EACnE,OAAOA,CACT,CAGO,SAASC,EACdC,EACAC,EAAqB,WAAW,SACxB,CACR,IAAMC,EAAWD,EAAS,OAASA,EAAS,SAC5C,GAAI,OAAOD,GAAW,WAAY,CAChC,IAAMG,EAAYH,EAAOC,CAAQ,EAC7BG,EACJ,GAAI,CACFA,EAAS,IAAI,IAAID,CAAS,CAC5B,MAAQ,CACN,eAAQ,MAAM,wDAAyD,CAAE,MAAOA,CAAU,CAAC,EACpFD,CACT,CACA,OAAIE,EAAO,WAAa,SAAWA,EAAO,WAAa,UAAYA,EAAO,OAAS,IACjF,QAAQ,MAAM,wDAAyD,CAAE,MAAOD,CAAU,CAAC,EACpFD,IAETE,EAAO,KAAO,GACVA,EAAO,SAAW,MAAKA,EAAO,OAAS,IACpCA,EAAO,OAASA,EAAO,SAAWA,EAAO,OAClD,CACA,OAAQJ,EAAQ,CACd,IAAK,OAAQ,CACX,IAAMI,EAAS,IAAI,IAAIH,EAAS,IAAI,EACpC,OAAAG,EAAO,KAAO,GACVA,EAAO,SAAW,MAAKA,EAAO,OAAS,IACpCA,EAAO,OAASA,EAAO,SAAWA,EAAO,MAClD,CACA,IAAK,SACH,OAAOH,EAAS,OAElB,QACE,OAAOC,CAEX,CACF,CAGO,SAASG,EAAOX,EAAmC,CACxD,OAAO,SAAoBY,EAA8B,CACvD,GAAM,CACJ,SAAAC,EACA,UAAAC,EAAY,QACZ,OAAAC,EACA,aAAAC,EAAe,IACf,WAAAC,EAAa,IACb,QAAAC,EAAU,GACV,IAAAC,EAAM,MACR,EAAIP,EACJ,GAAI,CAACC,GAAY,CAACD,EAAQ,KAAM,MAAM,IAAI,UAAU,uCAAuC,EAC3F,IAAIQ,EACJ,GAAIR,EAAQ,KAAMQ,EAAOR,EAAQ,SAC5B,IAAIC,IAAa,OAAW,MAAM,IAAI,UAAU,uCAAuC,EACvFO,EAAON,IAAc,UAAYO,EAAQR,EAAUE,CAAM,EAAIO,EAAMT,CAAQ,EAChF,IAAMU,EAAW,SAAS,gBAAgB,KACpC,CAACC,EAAQC,CAAQ,EAAI1B,EAAKC,EAASY,EAAQ,SAAWW,GAAY,UAAU,SAAS,EACrFG,EAAKd,EAAQ,KAAOe,GAASC,EAAQD,EAAOF,EAAUD,EAAQN,CAAO,GACvEW,EAAO,KACPC,EAAS,GACPC,EAAQ,MAAOC,GAAwC,CAC3D,GAAI,CAACC,EAAUD,CAAK,EAAG,OACvB,GAAIE,EAAWF,EAAM,MAAM,EACzB,OAAOpC,EAAM,+BAAgC,CAAE,OAAQoC,EAAM,MAAO,CAAC,EACvE,IAAML,EAAQQ,EAAQ,aAAa,EAAGnB,CAAY,EAClD,GAAI,CAACW,EAAO,OAAO/B,EAAM,8BAA+B,CAAE,aAAAoB,CAAa,CAAC,EACxE,IAAMoB,EAAOP,EAAOZ,EAAa,KAAK,IAAI,EAC1C,GAAImB,EAAO,EAAG,OAAOxC,EAAM,6BAA8B,CAAE,KAAAwC,CAAK,CAAC,EACjE,GAAIN,EAAQ,OAAOlC,EAAM,mCAAoC,CAAE,UAAW+B,EAAM,SAAU,CAAC,EAC3FK,EAAM,eAAe,EACrBF,EAAS,GACT,GAAI,CACF,IAAMO,EAAe,MAAMX,EAAGC,CAAK,EACnC,GAAI,CAACU,EAAc,OAAOzC,EAAM,YAAa,CAAE,UAAW+B,EAAM,SAAU,CAAC,EAC3EE,EAAO,KAAK,IAAI,EAChB,IAAMS,EAAOC,EAAUF,EAAa,SAAS,KAAK,GAAK,EAAE,EACnDG,EAAO,CAAE,GAAGb,EAAO,KAAMJ,GAAYC,EAAQ,GAAIc,GAAQ,CAAE,QAASA,CAAK,CAAG,EAClF,MAAMlB,EAAK,CAAE,KAAM,OAAQ,IAAK,EAAG,IAAKf,EAAWc,EAAK,QAAQ,EAAG,KAAAqB,CAAK,CAAC,EACzE5C,EAAM,OAAQ,CAAE,UAAW+B,EAAM,UAAW,UAAAb,EAAW,QAAS,EAAQwB,CAAM,CAAC,CACjF,OAASG,EAAO,CACd7C,EAAM,WAAY,CAAE,MAAA6C,CAAM,CAAC,CAC7B,QAAE,CACAX,EAAS,EACX,CACF,EACMY,EAAYV,GAA4B,CAAQD,EAAMC,CAAK,GACjE,gBAAS,iBAAiB,UAAWU,CAAQ,EAC7C9C,EAAM,YAAa,CAAE,OAAA4B,EAAQ,UAAAV,EAAW,WAAAG,CAAW,CAAC,EAC7C,IAAM,CACX,SAAS,oBAAoB,UAAWyB,CAAQ,EAChD9C,EAAM,WAAY,CAAE,OAAA4B,CAAO,CAAC,CAC9B,CACF,CACF,CCpHO,IAAMmB,GAAaC,EAAO,CAAE,GAAAC,EAAI,GAAAC,EAAI,GAAAC,CAAG,CAAC","names":["en_default","es_default","uk_default","EDITABLE","BLOCK","isTrigger","event","isEditable","target","MAX_TEXT_BYTES","clipBytes","text","max","encoder","clipped","squash","toDraft","before","selected","after","prefix","suffix","head","tail","renderedTextNodes","scope","nodes","walker","node","element","capture","selection","range","common","draft","beforeText","selectedText","from","to","afterText","hasStarted","CSS","element","tag","parent","text","node","confirm","draft","messages","lang","hasComment","host","root","sheet","dialog","form","context","label","comment","MAX_TEXT_BYTES","menu","send","resolve","post","endpoint","report","retryAfterMs","header","random","seconds","relay","response","wait","resolve","webhook","url","format","report","response","debug","message","data","pick","catalog","tag","base","found","first","resolveUrl","option","location","fallback","candidate","parsed","create","options","endpoint","transport","format","maxSelection","cooldownMs","comment","url","send","webhook","relay","pageLang","locale","messages","ui","draft","confirm","last","isOpen","onKey","event","isTrigger","isEditable","capture","wait","confirmation","note","clipBytes","body","error","listener","reportTypo","create","en_default","es_default","uk_default"]}
|
|
1
|
+
{"version":3,"sources":["../locales/en.yaml","../locales/es.yaml","../locales/uk.yaml","../src/capture.ts","../src/dialog.ts","../src/transport/relay.ts","../src/transport/webhook.ts","../src/core.ts","../src/index.ts"],"sourcesContent":["export default {\"dir\":\"ltr\",\"title\":\"Report a typo\",\"send\":\"Send\",\"cancel\":\"Cancel\",\"comment\":\"Comment (optional)\",\"commentPlaceholder\":\"What should it say?\"}","export default {\"dir\":\"ltr\",\"title\":\"Avisar de una errata\",\"send\":\"Enviar\",\"cancel\":\"Cancelar\",\"comment\":\"Comentario (opcional)\",\"commentPlaceholder\":\"¿Qué debería decir?\"}","export default {\"dir\":\"ltr\",\"title\":\"Повідомити про одруківку\",\"send\":\"Надіслати\",\"cancel\":\"Скасувати\",\"comment\":\"Коментар (необов’язково)\",\"commentPlaceholder\":\"Як має бути?\"}","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Draft } from './types.js'\n\n/** Characters of context kept on each side of the selection. */\nexport const CONTEXT_CHARS = 64\n\nconst EDITABLE = 'input,textarea,select,[contenteditable]:not([contenteditable=\"false\"])'\nconst BLOCK =\n 'p,li,dd,dt,td,th,blockquote,pre,figcaption,h1,h2,h3,h4,h5,h6,article,section,main,body'\n\n/** Reports whether a key event is Ctrl+Enter, or Cmd+Enter on macOS. */\nexport function isTrigger(event: Pick<KeyboardEvent, 'ctrlKey' | 'metaKey' | 'key'>): boolean {\n return (event.ctrlKey || event.metaKey) && event.key === 'Enter'\n}\n\n/** Reports whether the event target is a field the reader is typing in. */\nexport function isEditable(target: EventTarget | null): boolean {\n const element = target as Partial<Element> | null\n return Boolean(element?.closest?.(EDITABLE))\n}\n\n/** Bytes report-relay keeps of one free-text field. */\nexport const MAX_TEXT_BYTES = 1024\n\n/** Cuts text to a UTF-8 byte budget on a code point boundary. */\nexport function clipBytes(text: string, max = MAX_TEXT_BYTES): string {\n const encoder = new TextEncoder()\n let clipped = text\n while (encoder.encode(clipped).length > max) clipped = [...clipped].slice(0, -1).join('')\n return clipped\n}\n\nconst squash = (text: string): string => text.replaceAll(/\\s+/g, ' ')\n\n/** Builds a draft from the text before, inside and after a selection, cut to whole words. */\nexport function toDraft(before: string, selected: string, after: string, max: number): Draft {\n const prefix = squash(before)\n const suffix = squash(after)\n const head = prefix.slice(-CONTEXT_CHARS)\n const tail = suffix.slice(0, CONTEXT_CHARS)\n return {\n selection: squash(selected).trim().slice(0, max),\n prefix: (prefix.length > CONTEXT_CHARS ? head.replace(/^\\S*\\s/, '') : head).trimStart(),\n suffix: (suffix.length > CONTEXT_CHARS ? tail.replace(/\\s\\S*$/, '') : tail).trimEnd()\n }\n}\n\n/** Collects the rendered text nodes of a subtree, skipping non-rendered and hidden ones. */\nfunction renderedTextNodes(scope: Node): Text[] {\n const nodes: Text[] = []\n const walker = document.createTreeWalker(scope, 5, {\n acceptNode(node) {\n if (node.nodeType === 3) return 1\n const element = node as Element\n return '|SCRIPT|STYLE|TEMPLATE|NOSCRIPT|'.includes('|' + element.tagName + '|') ||\n element.hasAttribute('hidden') ||\n element.getAttribute('aria-hidden') === 'true'\n ? 2\n : 3\n }\n })\n for (let node = walker.firstChild(); node; node = walker.nextNode()) {\n if (node.nodeType === 3) nodes.push(node as Text)\n }\n return nodes\n}\n\n/** Reads the current selection and its surrounding text from the page. */\nexport function capture(selection: Selection | null, max: number): Draft | undefined {\n if (!selection || selection.isCollapsed || selection.rangeCount === 0) return\n const range = selection.getRangeAt(0)\n const common = range.commonAncestorContainer\n const scope =\n (common instanceof Element ? common : common.parentElement)?.closest(BLOCK) ?? document.body\n\n if (typeof range.comparePoint !== 'function') {\n const before = document.createRange()\n before.selectNodeContents(scope)\n before.setEnd(range.startContainer, range.startOffset)\n const after = document.createRange()\n after.selectNodeContents(scope)\n after.setStart(range.endContainer, range.endOffset)\n const draft = toDraft(before.toString(), range.toString(), after.toString(), max)\n return draft.selection ? draft : undefined\n }\n\n const nodes = renderedTextNodes(scope)\n\n let beforeText = ''\n for (const node of nodes) {\n if (node === range.startContainer) {\n beforeText += node.data.slice(0, range.startOffset)\n break\n }\n if (range.comparePoint(node, node.data.length) < 0) beforeText += node.data\n else break\n }\n\n let selectedText = ''\n for (const node of nodes) {\n if (range.comparePoint(node, node.data.length) < 0) continue\n if (range.comparePoint(node, 0) > 0) break\n let from = 0\n let to = node.data.length\n if (node === range.startContainer) from = range.startOffset\n if (node === range.endContainer) to = range.endOffset\n if (to > from) selectedText += node.data.slice(from, to)\n }\n if (!selectedText) selectedText = range.toString()\n\n let afterText = ''\n let hasStarted = false\n for (const node of nodes) {\n if (hasStarted) {\n afterText += node.data\n } else if (node === range.endContainer) {\n afterText += node.data.slice(range.endOffset)\n hasStarted = true\n } else if (range.comparePoint(node, 0) > 0) {\n afterText += node.data\n hasStarted = true\n }\n }\n\n const draft = toDraft(beforeText, selectedText, afterText, max)\n return draft.selection ? draft : undefined\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport { MAX_TEXT_BYTES } from './capture.js'\nimport type { Confirmation, Draft, Messages } from './types.js'\n\nconst CSS = `dialog{max-inline-size:min(32rem,90vw);font:var(--report-typo-font,1rem/1.5 system-ui,sans-serif);\ncolor:var(--report-typo-color,CanvasText);background:var(--report-typo-background,Canvas);\nborder:var(--report-typo-border,1px solid GrayText);border-radius:var(--report-typo-radius,.5rem)}\np,label{display:block;margin:0 0 1rem}mark{padding:0 .1em;background:var(--report-typo-highlight,Mark)}\ntextarea{display:block;box-sizing:border-box;inline-size:100%;font:inherit}\nmenu{display:flex;gap:.5rem;justify-content:end;margin:0;padding:0}`\n\nconst element = <K extends keyof HTMLElementTagNameMap>(\n tag: K,\n parent: ParentNode,\n text = ''\n): HTMLElementTagNameMap[K] => {\n const node = document.createElement(tag)\n node.textContent = text\n parent.append(node)\n return node\n}\n\n/** Shows the draft in a modal dialog inside a closed shadow root and waits for Send or Cancel. */\nexport function confirm(\n draft: Draft,\n messages: Messages,\n lang: string,\n hasComment: boolean\n): Promise<Confirmation | undefined> {\n const host = document.createElement('report-typo')\n const root = host.attachShadow({ mode: 'closed' })\n const sheet = new CSSStyleSheet()\n sheet.replaceSync(CSS)\n root.adoptedStyleSheets = [sheet]\n const dialog = element('dialog', root)\n dialog.dir = messages.dir\n dialog.lang = lang\n const form = element('form', dialog)\n form.method = 'dialog'\n element('h2', form, messages.title)\n const context = element('p', form, draft.prefix)\n element('mark', context, draft.selection)\n context.append(draft.suffix)\n const label = hasComment ? element('label', form, messages.comment) : undefined\n const comment = label ? element('textarea', label) : undefined\n if (comment) {\n comment.placeholder = messages.commentPlaceholder\n comment.maxLength = MAX_TEXT_BYTES\n comment.rows = 3\n }\n const menu = element('menu', form)\n element('button', menu, messages.cancel).value = 'cancel'\n const send = element('button', menu, messages.send)\n send.value = 'send'\n send.autofocus = !comment\n document.body.append(host)\n return new Promise(resolve => {\n dialog.addEventListener('close', () => {\n host.remove()\n resolve(dialog.returnValue === 'send' ? { comment: comment?.value } : undefined)\n })\n dialog.showModal()\n comment?.focus()\n })\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Send, TypoReport } from '../types.js'\n\n/** Per-attempt deadline of one delivery. */\nexport const TIMEOUT_MS = 5000\n\nconst post = (endpoint: string, report: TypoReport): Promise<Response> =>\n fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/reports+json' },\n body: JSON.stringify([report]),\n keepalive: true,\n credentials: 'omit',\n mode: 'cors',\n signal: AbortSignal.timeout(TIMEOUT_MS)\n })\n\n/** Milliseconds to wait on a `Retry-After` header, with up to 50% jitter; 1 s when absent or a date. */\nexport function retryAfterMs(header: string | null, random = Math.random): number {\n const seconds = Number(header)\n return (Number.isFinite(seconds) && seconds > 0 ? seconds : 1) * 1000 * (1 + random() / 2)\n}\n\n/** Sends reports to a report-relay intake, retrying once on 429. */\nexport function relay(endpoint: string): Send {\n return async report => {\n let response = await post(endpoint, report)\n if (response.status === 429) {\n const wait = retryAfterMs(response.headers.get('Retry-After'))\n console.debug('report-typo: relay busy, retrying', { status: response.status, wait })\n await new Promise(resolve => setTimeout(resolve, wait))\n response = await post(endpoint, report)\n }\n if (!response.ok) throw new Error(`report-relay answered ${response.status}`)\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Send, TypoReport } from '../types.js'\nimport { TIMEOUT_MS } from './relay.js'\n\n/** Turns a report into the JSON a webhook expects. */\nexport type Format = (report: TypoReport) => unknown\n\n/** Sends reports as a plain JSON POST, shaped by `format` when given. */\nexport function webhook(url: string, format: Format = report => report): Send {\n return async report => {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(format(report)),\n keepalive: true,\n credentials: 'omit',\n signal: AbortSignal.timeout(TIMEOUT_MS)\n })\n if (!response.ok) throw new Error(`webhook answered ${response.status}`)\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport { capture, clipBytes, isEditable, isTrigger } from './capture.js'\nimport { confirm } from './dialog.js'\nimport { relay } from './transport/relay.js'\nimport { webhook, type Format } from './transport/webhook.js'\nimport type { Messages, Send, Ui } from './types.js'\n\n/** Options of `reportTypo`; `endpoint` or `send` is required. */\nexport interface Options {\n endpoint?: string\n transport?: 'relay' | 'webhook'\n format?: Format\n send?: Send\n locale?: string\n maxSelection?: number\n cooldownMs?: number\n comment?: boolean\n ui?: Ui\n url?: 'full' | 'path' | 'origin' | ((location: Location) => string)\n}\n\nconst debug = (message: string, data: unknown): void =>\n console.debug(`report-typo: ${message}`, data)\n\n/** Picks the catalog entry for a language tag by its base subtag, falling back to the first entry. */\nexport function pick(catalog: Record<string, Messages>, tag: string): [string, Messages] {\n const base = tag.toLowerCase().split('-', 1)[0] ?? ''\n const found = catalog[base]\n if (found) return [base, found]\n const [first] = Object.entries(catalog)\n if (!first) throw new TypeError('report-typo: empty locale catalog')\n return first\n}\n\n/** Resolves the page URL to send, per the `url` option, dropping the fragment. */\nexport function resolveUrl(\n option: Options['url'],\n location: Location = globalThis.location\n): string {\n const fallback = location.origin + location.pathname\n if (typeof option === 'function') {\n const candidate = option(location)\n let parsed: URL\n try {\n parsed = new URL(candidate)\n } catch {\n console.debug('report-typo: invalid url option, falling back to path', { value: candidate })\n return fallback\n }\n if ((parsed.protocol !== 'http:' && parsed.protocol !== 'https:') || parsed.host === '') {\n console.debug('report-typo: invalid url option, falling back to path', { value: candidate })\n return fallback\n }\n parsed.hash = ''\n if (parsed.search === '?') parsed.search = ''\n return parsed.origin + parsed.pathname + parsed.search\n }\n switch (option) {\n case 'full': {\n const parsed = new URL(location.href)\n parsed.hash = ''\n if (parsed.search === '?') parsed.search = ''\n return parsed.origin + parsed.pathname + parsed.search\n }\n case 'origin': {\n return location.origin\n }\n default: {\n return fallback\n }\n }\n}\n\n/** Builds `reportTypo` over a locale catalog, so each bundle ships only the locales it imports. */\nexport function create(catalog: Record<string, Messages>) {\n return function reportTypo(options: Options): () => void {\n const {\n endpoint,\n transport = 'relay',\n format,\n maxSelection = 256,\n cooldownMs = 2000,\n comment = false,\n url = 'path'\n } = options\n if (!endpoint && !options.send) throw new TypeError('report-typo: set `endpoint` or `send`')\n let send: Send\n if (options.send) send = options.send\n else if (endpoint === undefined) throw new TypeError('report-typo: set `endpoint` or `send`')\n else send = transport === 'webhook' ? webhook(endpoint, format) : relay(endpoint)\n const pageLang = document.documentElement.lang\n const [locale, messages] = pick(catalog, options.locale ?? (pageLang || navigator.language))\n const ui = options.ui ?? (draft => confirm(draft, messages, locale, comment))\n let last = -Infinity\n let isOpen = false\n const onKey = async (event: KeyboardEvent): Promise<void> => {\n if (!isTrigger(event)) return\n if (isEditable(event.target))\n return debug('ignored in an editable field', { target: event.target })\n const draft = capture(getSelection(), maxSelection)\n if (!draft) return debug('ignored without a selection', { maxSelection })\n const wait = last + cooldownMs - Date.now()\n if (wait > 0) return debug('ignored while cooling down', { wait })\n if (isOpen) return debug('ignored while the dialog is open', { selection: draft.selection })\n event.preventDefault()\n isOpen = true\n try {\n const confirmation = await ui(draft)\n if (!confirmation) return debug('cancelled', { selection: draft.selection })\n last = Date.now()\n const note = clipBytes(confirmation.comment?.trim() ?? '')\n const body = { ...draft, lang: pageLang || locale, ...(note && { comment: note }) }\n await send({ type: 'typo', age: 0, url: resolveUrl(url, location), body })\n debug('sent', { selection: draft.selection, transport, comment: Boolean(note) })\n } catch (error) {\n debug('not sent', { error })\n } finally {\n isOpen = false\n }\n }\n const listener = (event: KeyboardEvent): void => void onKey(event)\n document.addEventListener('keydown', listener)\n debug('listening', { locale, transport, cooldownMs })\n return () => {\n document.removeEventListener('keydown', listener)\n debug('disposed', { locale })\n }\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport en from '../locales/en.yaml'\nimport es from '../locales/es.yaml'\nimport uk from '../locales/uk.yaml'\nimport { create } from './core.js'\n\nexport type { Options } from './core.js'\nexport type { Confirmation, Draft, Messages, Send, TypoBody, TypoReport, Ui } from './types.js'\nexport { relay } from './transport/relay.js'\nexport { webhook, type Format } from './transport/webhook.js'\n\n/** Starts listening for Ctrl+Enter and returns the function that stops it. */\nexport const reportTypo = create({ en, es, uk })\n"],"mappings":"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","names":["en_default","es_default","uk_default","EDITABLE","BLOCK","isTrigger","event","isEditable","target","MAX_TEXT_BYTES","clipBytes","text","max","encoder","clipped","squash","toDraft","before","selected","after","prefix","suffix","head","tail","renderedTextNodes","scope","nodes","walker","node","element","capture","selection","range","common","draft","beforeText","selectedText","from","to","afterText","hasStarted","CSS","element","tag","parent","text","node","confirm","draft","messages","lang","hasComment","host","root","sheet","dialog","form","context","label","comment","MAX_TEXT_BYTES","menu","send","resolve","post","endpoint","report","retryAfterMs","header","random","seconds","relay","response","wait","resolve","webhook","url","format","report","response","debug","message","data","pick","catalog","tag","base","found","first","resolveUrl","option","location","fallback","candidate","parsed","create","options","endpoint","transport","format","maxSelection","cooldownMs","comment","url","send","webhook","relay","pageLang","locale","messages","ui","draft","confirm","last","isOpen","onKey","event","isTrigger","isEditable","capture","wait","confirmation","note","clipBytes","body","error","listener","reportTypo","create","en_default","es_default","uk_default"]}
|
package/dist/uk.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../locales/uk.yaml","../src/capture.ts","../src/dialog.ts","../src/transport/relay.ts","../src/transport/webhook.ts","../src/core.ts","../src/entries/uk.ts"],"sourcesContent":["export default {\"dir\":\"ltr\",\"title\":\"Повідомити про одруківку\",\"send\":\"Надіслати\",\"cancel\":\"Скасувати\",\"comment\":\"Коментар (необов’язково)\",\"commentPlaceholder\":\"Як має бути?\"}","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Draft } from './types.js'\n\n/** Characters of context kept on each side of the selection. */\nexport const CONTEXT_CHARS = 64\n\nconst EDITABLE = 'input,textarea,select,[contenteditable]:not([contenteditable=\"false\"])'\nconst BLOCK =\n 'p,li,dd,dt,td,th,blockquote,pre,figcaption,h1,h2,h3,h4,h5,h6,article,section,main,body'\n\n/** Reports whether a key event is Ctrl+Enter, or Cmd+Enter on macOS. */\nexport function isTrigger(event: Pick<KeyboardEvent, 'ctrlKey' | 'metaKey' | 'key'>): boolean {\n return (event.ctrlKey || event.metaKey) && event.key === 'Enter'\n}\n\n/** Reports whether the event target is a field the reader is typing in. */\nexport function isEditable(target: EventTarget | null): boolean {\n const element = target as Partial<Element> | null\n return Boolean(element?.closest?.(EDITABLE))\n}\n\n/** Bytes report-relay keeps of one free-text field. */\nexport const MAX_TEXT_BYTES = 1024\n\n/** Cuts text to a UTF-8 byte budget on a code point boundary. */\nexport function clipBytes(text: string, max = MAX_TEXT_BYTES): string {\n const encoder = new TextEncoder()\n let clipped = text\n while (encoder.encode(clipped).length > max) clipped = [...clipped].slice(0, -1).join('')\n return clipped\n}\n\nconst squash = (text: string): string => text.replaceAll(/\\s+/g, ' ')\n\n/** Builds a draft from the text before, inside and after a selection, cut to whole words. */\nexport function toDraft(before: string, selected: string, after: string, max: number): Draft {\n const prefix = squash(before)\n const suffix = squash(after)\n const head = prefix.slice(-CONTEXT_CHARS)\n const tail = suffix.slice(0, CONTEXT_CHARS)\n return {\n selection: squash(selected).trim().slice(0, max),\n prefix: (prefix.length > CONTEXT_CHARS ? head.replace(/^\\S*\\s/, '') : head).trimStart(),\n suffix: (suffix.length > CONTEXT_CHARS ? tail.replace(/\\s\\S*$/, '') : tail).trimEnd()\n }\n}\n\n/** Collects the rendered text nodes of a subtree, skipping non-rendered and hidden ones. */\nfunction renderedTextNodes(scope: Node): Text[] {\n const nodes: Text[] = []\n const walker = document.createTreeWalker(scope, 5, {\n acceptNode(node) {\n if (node.nodeType === 3) return 1\n const element = node as Element\n return '|SCRIPT|STYLE|TEMPLATE|NOSCRIPT|'.includes('|' + element.tagName + '|') ||\n element.hasAttribute('hidden') ||\n element.getAttribute('aria-hidden') === 'true'\n ? 2\n : 3\n }\n })\n for (let node = walker.firstChild(); node; node = walker.nextNode()) {\n if (node.nodeType === 3) nodes.push(node as Text)\n }\n return nodes\n}\n\n/** Reads the current selection and its surrounding text from the page. */\nexport function capture(selection: Selection | null, max: number): Draft | undefined {\n if (!selection || selection.isCollapsed || selection.rangeCount === 0) return\n const range = selection.getRangeAt(0)\n const common = range.commonAncestorContainer\n const scope =\n (common instanceof Element ? common : common.parentElement)?.closest(BLOCK) ?? document.body\n\n if (typeof range.comparePoint !== 'function') {\n const before = document.createRange()\n before.selectNodeContents(scope)\n before.setEnd(range.startContainer, range.startOffset)\n const after = document.createRange()\n after.selectNodeContents(scope)\n after.setStart(range.endContainer, range.endOffset)\n const draft = toDraft(before.toString(), range.toString(), after.toString(), max)\n return draft.selection ? draft : undefined\n }\n\n const nodes = renderedTextNodes(scope)\n\n let beforeText = ''\n for (const node of nodes) {\n if (node === range.startContainer) {\n beforeText += node.data.slice(0, range.startOffset)\n break\n }\n if (range.comparePoint(node, node.data.length) < 0) beforeText += node.data\n else break\n }\n\n let selectedText = ''\n for (const node of nodes) {\n if (range.comparePoint(node, node.data.length) < 0) continue\n if (range.comparePoint(node, 0) > 0) break\n let from = 0\n let to = node.data.length\n if (node === range.startContainer) from = range.startOffset\n if (node === range.endContainer) to = range.endOffset\n if (to > from) selectedText += node.data.slice(from, to)\n }\n if (!selectedText) selectedText = range.toString()\n\n let afterText = ''\n let hasStarted = false\n for (const node of nodes) {\n if (hasStarted) {\n afterText += node.data\n } else if (node === range.endContainer) {\n afterText += node.data.slice(range.endOffset)\n hasStarted = true\n } else if (range.comparePoint(node, 0) > 0) {\n afterText += node.data\n hasStarted = true\n }\n }\n\n const draft = toDraft(beforeText, selectedText, afterText, max)\n return draft.selection ? draft : undefined\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport { MAX_TEXT_BYTES } from './capture.js'\nimport type { Confirmation, Draft, Messages } from './types.js'\n\nconst CSS = `dialog{max-inline-size:min(32rem,90vw);font:var(--report-typo-font,1rem/1.5 system-ui,sans-serif);\ncolor:var(--report-typo-color,CanvasText);background:var(--report-typo-background,Canvas);\nborder:var(--report-typo-border,1px solid GrayText);border-radius:var(--report-typo-radius,.5rem)}\np,label{display:block;margin:0 0 1rem}mark{padding:0 .1em;background:var(--report-typo-highlight,Mark)}\ntextarea{display:block;box-sizing:border-box;inline-size:100%;font:inherit}\nmenu{display:flex;gap:.5rem;justify-content:end;margin:0;padding:0}`\n\nconst element = <K extends keyof HTMLElementTagNameMap>(\n tag: K,\n parent: ParentNode,\n text = ''\n): HTMLElementTagNameMap[K] => {\n const node = document.createElement(tag)\n node.textContent = text\n parent.append(node)\n return node\n}\n\n/** Shows the draft in a modal dialog inside a closed shadow root and waits for Send or Cancel. */\nexport function confirm(\n draft: Draft,\n messages: Messages,\n lang: string,\n hasComment: boolean\n): Promise<Confirmation | undefined> {\n const host = document.createElement('report-typo')\n const root = host.attachShadow({ mode: 'closed' })\n const sheet = new CSSStyleSheet()\n sheet.replaceSync(CSS)\n root.adoptedStyleSheets = [sheet]\n const dialog = element('dialog', root)\n dialog.dir = messages.dir\n dialog.lang = lang\n const form = element('form', dialog)\n form.method = 'dialog'\n element('h2', form, messages.title)\n const context = element('p', form, draft.prefix)\n element('mark', context, draft.selection)\n context.append(draft.suffix)\n const label = hasComment ? element('label', form, messages.comment) : undefined\n const comment = label ? element('textarea', label) : undefined\n if (comment) {\n comment.placeholder = messages.commentPlaceholder\n comment.maxLength = MAX_TEXT_BYTES\n comment.rows = 3\n }\n const menu = element('menu', form)\n element('button', menu, messages.cancel).value = 'cancel'\n const send = element('button', menu, messages.send)\n send.value = 'send'\n send.autofocus = !comment\n document.body.append(host)\n return new Promise(resolve => {\n dialog.addEventListener('close', () => {\n host.remove()\n resolve(dialog.returnValue === 'send' ? { comment: comment?.value } : undefined)\n })\n dialog.showModal()\n comment?.focus()\n })\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Send, TypoReport } from '../types.js'\n\n/** Per-attempt deadline of one delivery. */\nexport const TIMEOUT_MS = 5000\n\nconst post = (endpoint: string, report: TypoReport): Promise<Response> =>\n fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/reports+json' },\n body: JSON.stringify([report]),\n keepalive: true,\n credentials: 'omit',\n mode: 'cors',\n signal: AbortSignal.timeout(TIMEOUT_MS)\n })\n\n/** Milliseconds to wait on a `Retry-After` header, with up to 50% jitter; 1 s when absent or a date. */\nexport function retryAfterMs(header: string | null, random = Math.random): number {\n const seconds = Number(header)\n return (Number.isFinite(seconds) && seconds > 0 ? seconds : 1) * 1000 * (1 + random() / 2)\n}\n\n/** Sends reports to a report-relay intake, retrying once on 429. */\nexport function relay(endpoint: string): Send {\n return async report => {\n let response = await post(endpoint, report)\n if (response.status === 429) {\n const wait = retryAfterMs(response.headers.get('Retry-After'))\n console.debug('report-typo: relay busy, retrying', { status: response.status, wait })\n await new Promise(resolve => setTimeout(resolve, wait))\n response = await post(endpoint, report)\n }\n if (!response.ok) throw new Error(`report-relay answered ${response.status}`)\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Send, TypoReport } from '../types.js'\nimport { TIMEOUT_MS } from './relay.js'\n\n/** Turns a report into the JSON a webhook expects. */\nexport type Format = (report: TypoReport) => unknown\n\n/** Sends reports as a plain JSON POST, shaped by `format` when given. */\nexport function webhook(url: string, format: Format = report => report): Send {\n return async report => {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(format(report)),\n keepalive: true,\n credentials: 'omit',\n signal: AbortSignal.timeout(TIMEOUT_MS)\n })\n if (!response.ok) throw new Error(`webhook answered ${response.status}`)\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport { capture, clipBytes, isEditable, isTrigger } from './capture.js'\nimport { confirm } from './dialog.js'\nimport { relay } from './transport/relay.js'\nimport { webhook, type Format } from './transport/webhook.js'\nimport type { Messages, Send, Ui } from './types.js'\n\n/** Options of `reportTypo`; `endpoint` or `send` is required. */\nexport interface Options {\n endpoint?: string\n transport?: 'relay' | 'webhook'\n format?: Format\n send?: Send\n locale?: string\n maxSelection?: number\n cooldownMs?: number\n comment?: boolean\n ui?: Ui\n url?: 'full' | 'path' | 'origin' | ((location: Location) => string)\n}\n\nconst debug = (message: string, data: unknown): void =>\n console.debug(`report-typo: ${message}`, data)\n\n/** Picks the catalog entry for a language tag by its base subtag, falling back to the first entry. */\nexport function pick(catalog: Record<string, Messages>, tag: string): [string, Messages] {\n const base = tag.toLowerCase().split('-', 1)[0] ?? ''\n const found = catalog[base]\n if (found) return [base, found]\n const [first] = Object.entries(catalog)\n if (!first) throw new TypeError('report-typo: empty locale catalog')\n return first\n}\n\n/** Resolves the page URL to send, per the `url` option, dropping the fragment. */\nexport function resolveUrl(\n option: Options['url'],\n location: Location = globalThis.location\n): string {\n const fallback = location.origin + location.pathname\n if (typeof option === 'function') {\n const candidate = option(location)\n let parsed: URL\n try {\n parsed = new URL(candidate)\n } catch {\n console.debug('report-typo: invalid url option, falling back to path', { value: candidate })\n return fallback\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:' || parsed.host === '') {\n console.debug('report-typo: invalid url option, falling back to path', { value: candidate })\n return fallback\n }\n parsed.hash = ''\n if (parsed.search === '?') parsed.search = ''\n return parsed.origin + parsed.pathname + parsed.search\n }\n switch (option) {\n case 'full': {\n const parsed = new URL(location.href)\n parsed.hash = ''\n if (parsed.search === '?') parsed.search = ''\n return parsed.origin + parsed.pathname + parsed.search\n }\n case 'origin': {\n return location.origin\n }\n default: {\n return fallback\n }\n }\n}\n\n/** Builds `reportTypo` over a locale catalog, so each bundle ships only the locales it imports. */\nexport function create(catalog: Record<string, Messages>) {\n return function reportTypo(options: Options): () => void {\n const {\n endpoint,\n transport = 'relay',\n format,\n maxSelection = 256,\n cooldownMs = 2000,\n comment = false,\n url = 'path'\n } = options\n if (!endpoint && !options.send) throw new TypeError('report-typo: set `endpoint` or `send`')\n let send: Send\n if (options.send) send = options.send\n else if (endpoint === undefined) throw new TypeError('report-typo: set `endpoint` or `send`')\n else send = transport === 'webhook' ? webhook(endpoint, format) : relay(endpoint)\n const pageLang = document.documentElement.lang\n const [locale, messages] = pick(catalog, options.locale ?? (pageLang || navigator.language))\n const ui = options.ui ?? (draft => confirm(draft, messages, locale, comment))\n let last = -Infinity\n let isOpen = false\n const onKey = async (event: KeyboardEvent): Promise<void> => {\n if (!isTrigger(event)) return\n if (isEditable(event.target))\n return debug('ignored in an editable field', { target: event.target })\n const draft = capture(getSelection(), maxSelection)\n if (!draft) return debug('ignored without a selection', { maxSelection })\n const wait = last + cooldownMs - Date.now()\n if (wait > 0) return debug('ignored while cooling down', { wait })\n if (isOpen) return debug('ignored while the dialog is open', { selection: draft.selection })\n event.preventDefault()\n isOpen = true\n try {\n const confirmation = await ui(draft)\n if (!confirmation) return debug('cancelled', { selection: draft.selection })\n last = Date.now()\n const note = clipBytes(confirmation.comment?.trim() ?? '')\n const body = { ...draft, lang: pageLang || locale, ...(note && { comment: note }) }\n await send({ type: 'typo', age: 0, url: resolveUrl(url, location), body })\n debug('sent', { selection: draft.selection, transport, comment: Boolean(note) })\n } catch (error) {\n debug('not sent', { error })\n } finally {\n isOpen = false\n }\n }\n const listener = (event: KeyboardEvent): void => void onKey(event)\n document.addEventListener('keydown', listener)\n debug('listening', { locale, transport, cooldownMs })\n return () => {\n document.removeEventListener('keydown', listener)\n debug('disposed', { locale })\n }\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport uk from '../../locales/uk.yaml'\nimport { create } from '../core.js'\n\nexport type { Options } from '../core.js'\nexport type { Confirmation, Draft, Messages, Send, TypoBody, TypoReport, Ui } from '../types.js'\nexport { relay } from '../transport/relay.js'\nexport { webhook, type Format } from '../transport/webhook.js'\n\n/** Starts listening for Ctrl+Enter and returns the function that stops it. */\nexport const reportTypo = create({ uk })\n"],"mappings":"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","names":["uk_default","EDITABLE","BLOCK","isTrigger","event","isEditable","target","MAX_TEXT_BYTES","clipBytes","text","max","encoder","clipped","squash","toDraft","before","selected","after","prefix","suffix","head","tail","renderedTextNodes","scope","nodes","walker","node","element","capture","selection","range","common","draft","beforeText","selectedText","from","to","afterText","hasStarted","CSS","element","tag","parent","text","node","confirm","draft","messages","lang","hasComment","host","root","sheet","dialog","form","context","label","comment","MAX_TEXT_BYTES","menu","send","resolve","post","endpoint","report","retryAfterMs","header","random","seconds","relay","response","wait","resolve","webhook","url","format","report","response","debug","message","data","pick","catalog","tag","base","found","first","resolveUrl","option","location","fallback","candidate","parsed","create","options","endpoint","transport","format","maxSelection","cooldownMs","comment","url","send","webhook","relay","pageLang","locale","messages","ui","draft","confirm","last","isOpen","onKey","event","isTrigger","isEditable","capture","wait","confirmation","note","clipBytes","body","error","listener","reportTypo","create","uk_default"]}
|
|
1
|
+
{"version":3,"sources":["../locales/uk.yaml","../src/capture.ts","../src/dialog.ts","../src/transport/relay.ts","../src/transport/webhook.ts","../src/core.ts","../src/entries/uk.ts"],"sourcesContent":["export default {\"dir\":\"ltr\",\"title\":\"Повідомити про одруківку\",\"send\":\"Надіслати\",\"cancel\":\"Скасувати\",\"comment\":\"Коментар (необов’язково)\",\"commentPlaceholder\":\"Як має бути?\"}","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Draft } from './types.js'\n\n/** Characters of context kept on each side of the selection. */\nexport const CONTEXT_CHARS = 64\n\nconst EDITABLE = 'input,textarea,select,[contenteditable]:not([contenteditable=\"false\"])'\nconst BLOCK =\n 'p,li,dd,dt,td,th,blockquote,pre,figcaption,h1,h2,h3,h4,h5,h6,article,section,main,body'\n\n/** Reports whether a key event is Ctrl+Enter, or Cmd+Enter on macOS. */\nexport function isTrigger(event: Pick<KeyboardEvent, 'ctrlKey' | 'metaKey' | 'key'>): boolean {\n return (event.ctrlKey || event.metaKey) && event.key === 'Enter'\n}\n\n/** Reports whether the event target is a field the reader is typing in. */\nexport function isEditable(target: EventTarget | null): boolean {\n const element = target as Partial<Element> | null\n return Boolean(element?.closest?.(EDITABLE))\n}\n\n/** Bytes report-relay keeps of one free-text field. */\nexport const MAX_TEXT_BYTES = 1024\n\n/** Cuts text to a UTF-8 byte budget on a code point boundary. */\nexport function clipBytes(text: string, max = MAX_TEXT_BYTES): string {\n const encoder = new TextEncoder()\n let clipped = text\n while (encoder.encode(clipped).length > max) clipped = [...clipped].slice(0, -1).join('')\n return clipped\n}\n\nconst squash = (text: string): string => text.replaceAll(/\\s+/g, ' ')\n\n/** Builds a draft from the text before, inside and after a selection, cut to whole words. */\nexport function toDraft(before: string, selected: string, after: string, max: number): Draft {\n const prefix = squash(before)\n const suffix = squash(after)\n const head = prefix.slice(-CONTEXT_CHARS)\n const tail = suffix.slice(0, CONTEXT_CHARS)\n return {\n selection: squash(selected).trim().slice(0, max),\n prefix: (prefix.length > CONTEXT_CHARS ? head.replace(/^\\S*\\s/, '') : head).trimStart(),\n suffix: (suffix.length > CONTEXT_CHARS ? tail.replace(/\\s\\S*$/, '') : tail).trimEnd()\n }\n}\n\n/** Collects the rendered text nodes of a subtree, skipping non-rendered and hidden ones. */\nfunction renderedTextNodes(scope: Node): Text[] {\n const nodes: Text[] = []\n const walker = document.createTreeWalker(scope, 5, {\n acceptNode(node) {\n if (node.nodeType === 3) return 1\n const element = node as Element\n return '|SCRIPT|STYLE|TEMPLATE|NOSCRIPT|'.includes('|' + element.tagName + '|') ||\n element.hasAttribute('hidden') ||\n element.getAttribute('aria-hidden') === 'true'\n ? 2\n : 3\n }\n })\n for (let node = walker.firstChild(); node; node = walker.nextNode()) {\n if (node.nodeType === 3) nodes.push(node as Text)\n }\n return nodes\n}\n\n/** Reads the current selection and its surrounding text from the page. */\nexport function capture(selection: Selection | null, max: number): Draft | undefined {\n if (!selection || selection.isCollapsed || selection.rangeCount === 0) return\n const range = selection.getRangeAt(0)\n const common = range.commonAncestorContainer\n const scope =\n (common instanceof Element ? common : common.parentElement)?.closest(BLOCK) ?? document.body\n\n if (typeof range.comparePoint !== 'function') {\n const before = document.createRange()\n before.selectNodeContents(scope)\n before.setEnd(range.startContainer, range.startOffset)\n const after = document.createRange()\n after.selectNodeContents(scope)\n after.setStart(range.endContainer, range.endOffset)\n const draft = toDraft(before.toString(), range.toString(), after.toString(), max)\n return draft.selection ? draft : undefined\n }\n\n const nodes = renderedTextNodes(scope)\n\n let beforeText = ''\n for (const node of nodes) {\n if (node === range.startContainer) {\n beforeText += node.data.slice(0, range.startOffset)\n break\n }\n if (range.comparePoint(node, node.data.length) < 0) beforeText += node.data\n else break\n }\n\n let selectedText = ''\n for (const node of nodes) {\n if (range.comparePoint(node, node.data.length) < 0) continue\n if (range.comparePoint(node, 0) > 0) break\n let from = 0\n let to = node.data.length\n if (node === range.startContainer) from = range.startOffset\n if (node === range.endContainer) to = range.endOffset\n if (to > from) selectedText += node.data.slice(from, to)\n }\n if (!selectedText) selectedText = range.toString()\n\n let afterText = ''\n let hasStarted = false\n for (const node of nodes) {\n if (hasStarted) {\n afterText += node.data\n } else if (node === range.endContainer) {\n afterText += node.data.slice(range.endOffset)\n hasStarted = true\n } else if (range.comparePoint(node, 0) > 0) {\n afterText += node.data\n hasStarted = true\n }\n }\n\n const draft = toDraft(beforeText, selectedText, afterText, max)\n return draft.selection ? draft : undefined\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport { MAX_TEXT_BYTES } from './capture.js'\nimport type { Confirmation, Draft, Messages } from './types.js'\n\nconst CSS = `dialog{max-inline-size:min(32rem,90vw);font:var(--report-typo-font,1rem/1.5 system-ui,sans-serif);\ncolor:var(--report-typo-color,CanvasText);background:var(--report-typo-background,Canvas);\nborder:var(--report-typo-border,1px solid GrayText);border-radius:var(--report-typo-radius,.5rem)}\np,label{display:block;margin:0 0 1rem}mark{padding:0 .1em;background:var(--report-typo-highlight,Mark)}\ntextarea{display:block;box-sizing:border-box;inline-size:100%;font:inherit}\nmenu{display:flex;gap:.5rem;justify-content:end;margin:0;padding:0}`\n\nconst element = <K extends keyof HTMLElementTagNameMap>(\n tag: K,\n parent: ParentNode,\n text = ''\n): HTMLElementTagNameMap[K] => {\n const node = document.createElement(tag)\n node.textContent = text\n parent.append(node)\n return node\n}\n\n/** Shows the draft in a modal dialog inside a closed shadow root and waits for Send or Cancel. */\nexport function confirm(\n draft: Draft,\n messages: Messages,\n lang: string,\n hasComment: boolean\n): Promise<Confirmation | undefined> {\n const host = document.createElement('report-typo')\n const root = host.attachShadow({ mode: 'closed' })\n const sheet = new CSSStyleSheet()\n sheet.replaceSync(CSS)\n root.adoptedStyleSheets = [sheet]\n const dialog = element('dialog', root)\n dialog.dir = messages.dir\n dialog.lang = lang\n const form = element('form', dialog)\n form.method = 'dialog'\n element('h2', form, messages.title)\n const context = element('p', form, draft.prefix)\n element('mark', context, draft.selection)\n context.append(draft.suffix)\n const label = hasComment ? element('label', form, messages.comment) : undefined\n const comment = label ? element('textarea', label) : undefined\n if (comment) {\n comment.placeholder = messages.commentPlaceholder\n comment.maxLength = MAX_TEXT_BYTES\n comment.rows = 3\n }\n const menu = element('menu', form)\n element('button', menu, messages.cancel).value = 'cancel'\n const send = element('button', menu, messages.send)\n send.value = 'send'\n send.autofocus = !comment\n document.body.append(host)\n return new Promise(resolve => {\n dialog.addEventListener('close', () => {\n host.remove()\n resolve(dialog.returnValue === 'send' ? { comment: comment?.value } : undefined)\n })\n dialog.showModal()\n comment?.focus()\n })\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Send, TypoReport } from '../types.js'\n\n/** Per-attempt deadline of one delivery. */\nexport const TIMEOUT_MS = 5000\n\nconst post = (endpoint: string, report: TypoReport): Promise<Response> =>\n fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/reports+json' },\n body: JSON.stringify([report]),\n keepalive: true,\n credentials: 'omit',\n mode: 'cors',\n signal: AbortSignal.timeout(TIMEOUT_MS)\n })\n\n/** Milliseconds to wait on a `Retry-After` header, with up to 50% jitter; 1 s when absent or a date. */\nexport function retryAfterMs(header: string | null, random = Math.random): number {\n const seconds = Number(header)\n return (Number.isFinite(seconds) && seconds > 0 ? seconds : 1) * 1000 * (1 + random() / 2)\n}\n\n/** Sends reports to a report-relay intake, retrying once on 429. */\nexport function relay(endpoint: string): Send {\n return async report => {\n let response = await post(endpoint, report)\n if (response.status === 429) {\n const wait = retryAfterMs(response.headers.get('Retry-After'))\n console.debug('report-typo: relay busy, retrying', { status: response.status, wait })\n await new Promise(resolve => setTimeout(resolve, wait))\n response = await post(endpoint, report)\n }\n if (!response.ok) throw new Error(`report-relay answered ${response.status}`)\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport type { Send, TypoReport } from '../types.js'\nimport { TIMEOUT_MS } from './relay.js'\n\n/** Turns a report into the JSON a webhook expects. */\nexport type Format = (report: TypoReport) => unknown\n\n/** Sends reports as a plain JSON POST, shaped by `format` when given. */\nexport function webhook(url: string, format: Format = report => report): Send {\n return async report => {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(format(report)),\n keepalive: true,\n credentials: 'omit',\n signal: AbortSignal.timeout(TIMEOUT_MS)\n })\n if (!response.ok) throw new Error(`webhook answered ${response.status}`)\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport { capture, clipBytes, isEditable, isTrigger } from './capture.js'\nimport { confirm } from './dialog.js'\nimport { relay } from './transport/relay.js'\nimport { webhook, type Format } from './transport/webhook.js'\nimport type { Messages, Send, Ui } from './types.js'\n\n/** Options of `reportTypo`; `endpoint` or `send` is required. */\nexport interface Options {\n endpoint?: string\n transport?: 'relay' | 'webhook'\n format?: Format\n send?: Send\n locale?: string\n maxSelection?: number\n cooldownMs?: number\n comment?: boolean\n ui?: Ui\n url?: 'full' | 'path' | 'origin' | ((location: Location) => string)\n}\n\nconst debug = (message: string, data: unknown): void =>\n console.debug(`report-typo: ${message}`, data)\n\n/** Picks the catalog entry for a language tag by its base subtag, falling back to the first entry. */\nexport function pick(catalog: Record<string, Messages>, tag: string): [string, Messages] {\n const base = tag.toLowerCase().split('-', 1)[0] ?? ''\n const found = catalog[base]\n if (found) return [base, found]\n const [first] = Object.entries(catalog)\n if (!first) throw new TypeError('report-typo: empty locale catalog')\n return first\n}\n\n/** Resolves the page URL to send, per the `url` option, dropping the fragment. */\nexport function resolveUrl(\n option: Options['url'],\n location: Location = globalThis.location\n): string {\n const fallback = location.origin + location.pathname\n if (typeof option === 'function') {\n const candidate = option(location)\n let parsed: URL\n try {\n parsed = new URL(candidate)\n } catch {\n console.debug('report-typo: invalid url option, falling back to path', { value: candidate })\n return fallback\n }\n if ((parsed.protocol !== 'http:' && parsed.protocol !== 'https:') || parsed.host === '') {\n console.debug('report-typo: invalid url option, falling back to path', { value: candidate })\n return fallback\n }\n parsed.hash = ''\n if (parsed.search === '?') parsed.search = ''\n return parsed.origin + parsed.pathname + parsed.search\n }\n switch (option) {\n case 'full': {\n const parsed = new URL(location.href)\n parsed.hash = ''\n if (parsed.search === '?') parsed.search = ''\n return parsed.origin + parsed.pathname + parsed.search\n }\n case 'origin': {\n return location.origin\n }\n default: {\n return fallback\n }\n }\n}\n\n/** Builds `reportTypo` over a locale catalog, so each bundle ships only the locales it imports. */\nexport function create(catalog: Record<string, Messages>) {\n return function reportTypo(options: Options): () => void {\n const {\n endpoint,\n transport = 'relay',\n format,\n maxSelection = 256,\n cooldownMs = 2000,\n comment = false,\n url = 'path'\n } = options\n if (!endpoint && !options.send) throw new TypeError('report-typo: set `endpoint` or `send`')\n let send: Send\n if (options.send) send = options.send\n else if (endpoint === undefined) throw new TypeError('report-typo: set `endpoint` or `send`')\n else send = transport === 'webhook' ? webhook(endpoint, format) : relay(endpoint)\n const pageLang = document.documentElement.lang\n const [locale, messages] = pick(catalog, options.locale ?? (pageLang || navigator.language))\n const ui = options.ui ?? (draft => confirm(draft, messages, locale, comment))\n let last = -Infinity\n let isOpen = false\n const onKey = async (event: KeyboardEvent): Promise<void> => {\n if (!isTrigger(event)) return\n if (isEditable(event.target))\n return debug('ignored in an editable field', { target: event.target })\n const draft = capture(getSelection(), maxSelection)\n if (!draft) return debug('ignored without a selection', { maxSelection })\n const wait = last + cooldownMs - Date.now()\n if (wait > 0) return debug('ignored while cooling down', { wait })\n if (isOpen) return debug('ignored while the dialog is open', { selection: draft.selection })\n event.preventDefault()\n isOpen = true\n try {\n const confirmation = await ui(draft)\n if (!confirmation) return debug('cancelled', { selection: draft.selection })\n last = Date.now()\n const note = clipBytes(confirmation.comment?.trim() ?? '')\n const body = { ...draft, lang: pageLang || locale, ...(note && { comment: note }) }\n await send({ type: 'typo', age: 0, url: resolveUrl(url, location), body })\n debug('sent', { selection: draft.selection, transport, comment: Boolean(note) })\n } catch (error) {\n debug('not sent', { error })\n } finally {\n isOpen = false\n }\n }\n const listener = (event: KeyboardEvent): void => void onKey(event)\n document.addEventListener('keydown', listener)\n debug('listening', { locale, transport, cooldownMs })\n return () => {\n document.removeEventListener('keydown', listener)\n debug('disposed', { locale })\n }\n }\n}\n","// SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>\n//\n// SPDX-License-Identifier: MIT\n\nimport uk from '../../locales/uk.yaml'\nimport { create } from '../core.js'\n\nexport type { Options } from '../core.js'\nexport type { Confirmation, Draft, Messages, Send, TypoBody, TypoReport, Ui } from '../types.js'\nexport { relay } from '../transport/relay.js'\nexport { webhook, type Format } from '../transport/webhook.js'\n\n/** Starts listening for Ctrl+Enter and returns the function that stops it. */\nexport const reportTypo = create({ uk })\n"],"mappings":"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","names":["uk_default","EDITABLE","BLOCK","isTrigger","event","isEditable","target","MAX_TEXT_BYTES","clipBytes","text","max","encoder","clipped","squash","toDraft","before","selected","after","prefix","suffix","head","tail","renderedTextNodes","scope","nodes","walker","node","element","capture","selection","range","common","draft","beforeText","selectedText","from","to","afterText","hasStarted","CSS","element","tag","parent","text","node","confirm","draft","messages","lang","hasComment","host","root","sheet","dialog","form","context","label","comment","MAX_TEXT_BYTES","menu","send","resolve","post","endpoint","report","retryAfterMs","header","random","seconds","relay","response","wait","resolve","webhook","url","format","report","response","debug","message","data","pick","catalog","tag","base","found","first","resolveUrl","option","location","fallback","candidate","parsed","create","options","endpoint","transport","format","maxSelection","cooldownMs","comment","url","send","webhook","relay","pageLang","locale","messages","ui","draft","confirm","last","isOpen","onKey","event","isTrigger","isEditable","capture","wait","confirmation","note","clipBytes","body","error","listener","reportTypo","create","uk_default"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "report-typo",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Ctrl+Enter typo reporting for any website",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"esbuild": "0.28.2",
|
|
91
91
|
"eslint": "10.14.0",
|
|
92
92
|
"eslint-config-prettier": "10.1.8",
|
|
93
|
-
"eslint-plugin-import-x": "4.
|
|
93
|
+
"eslint-plugin-import-x": "4.18.0",
|
|
94
94
|
"eslint-plugin-n": "18.5.0",
|
|
95
95
|
"eslint-plugin-promise": "7.3.0",
|
|
96
96
|
"eslint-plugin-tsdoc": "0.5.4",
|
|
@@ -118,6 +118,7 @@
|
|
|
118
118
|
"overrides": {
|
|
119
119
|
"esbuild": "0.28.2",
|
|
120
120
|
"globals": "$globals",
|
|
121
|
+
"minimatch@>=7.0.0 <11.0.1": "11.0.5",
|
|
121
122
|
"qs": "6.16.1"
|
|
122
123
|
},
|
|
123
124
|
"size-limit": [
|