report-typo 0.0.0-stage → 0.2.0
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/LICENSE +22 -0
- package/README.md +57 -2
- package/dist/capture.d.ts +15 -0
- package/dist/core.d.ts +18 -0
- package/dist/dialog.d.ts +3 -0
- package/dist/en.js +7 -0
- package/dist/en.js.map +1 -0
- package/dist/entries/en.d.ts +6 -0
- package/dist/entries/es.d.ts +6 -0
- package/dist/entries/uk.d.ts +6 -0
- package/dist/es.js +7 -0
- package/dist/es.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/transport/relay.d.ts +7 -0
- package/dist/transport/webhook.d.ts +5 -0
- package/dist/types.d.ts +35 -0
- package/dist/uk.js +7 -0
- package/dist/uk.js.map +1 -0
- package/package.json +119 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2026 Damián Búho
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
|
10
|
+
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
|
11
|
+
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
|
13
|
+
following conditions:
|
|
14
|
+
|
|
15
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial
|
|
16
|
+
portions of the Software.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
19
|
+
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
20
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
21
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
22
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,58 @@
|
|
|
1
|
-
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Damián Búho <damian.buho@proton.me>
|
|
3
|
+
SPDX-License-Identifier: MIT
|
|
4
|
+
pf-cli-managed: yes
|
|
5
|
+
-->
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
[Español](docs/es/README.md) · [Українська](docs/uk/README.md)
|
|
8
|
+
|
|
9
|
+
# Report Typo
|
|
10
|
+
|
|
11
|
+
A browser library that lets readers report a typo by selecting it and pressing Ctrl+Enter (Cmd+Enter on macOS). The report, with the text around the selection, goes to a report-relay endpoint, a webhook, or a function of your own.
|
|
12
|
+
|
|
13
|
+
[](https://damian-buho.github.io/support-ukraine/) [](https://projectfile.org) [](LICENSE) [](CONTRIBUTING.md) [](https://api.reuse.software/info/codeberg.org/damian-buho/report-typo)
|
|
14
|
+
|
|
15
|
+
 [](https://kiota.ch/damian-buho/report-typo) [](https://codeberg.org/damian-buho/report-typo) [](https://github.com/damian-buho/report-typo)
|
|
16
|
+
|
|
17
|
+
[](https://www.npmjs.com/package/report-typo) [](https://www.npmjs.com/package/report-typo) [](https://bundlephobia.com/package/report-typo) [](https://libraries.io/npm/report-typo)
|
|
18
|
+
|
|
19
|
+
[](https://github.com/damian-buho/report-typo/actions) [](https://github.com/damian-buho/report-typo/actions) [](https://github.com/damian-buho/report-typo/actions) [](https://github.com/damian-buho/report-typo/actions)
|
|
20
|
+
|
|
21
|
+
[](https://kiota.ch/damian-buho/report-typo/actions) [](https://kiota.ch/damian-buho/report-typo/actions) [](https://kiota.ch/damian-buho/report-typo/actions) [](https://kiota.ch/damian-buho/report-typo/actions)
|
|
22
|
+
|
|
23
|
+
## What this provides
|
|
24
|
+
|
|
25
|
+
- **npm package** `report-typo`
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
Add the package to your project:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
npm install report-typo
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Building
|
|
36
|
+
|
|
37
|
+
Clone the repository and run `make` — every build step, target and entry point is in [Building](docs/BUILD.md).
|
|
38
|
+
|
|
39
|
+
## Documentation
|
|
40
|
+
|
|
41
|
+
- [Add typo reporting to a site](docs/how-to/usage.md)
|
|
42
|
+
|
|
43
|
+
## Policies
|
|
44
|
+
|
|
45
|
+
- [How to contribute](CONTRIBUTING.md)
|
|
46
|
+
- [Security policy](SECURITY.md)
|
|
47
|
+
- [Getting support](SUPPORT.md)
|
|
48
|
+
- [Code of Conduct](CODE_OF_CONDUCT.md)
|
|
49
|
+
- [AI and LLM Policy](AI_POLICY.md)
|
|
50
|
+
|
|
51
|
+
## Links
|
|
52
|
+
|
|
53
|
+
- [Issues on GitHub](https://github.com/damian-buho/report-typo/issues)
|
|
54
|
+
- [Projectfile Specification](https://projectfile.org)
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
This project is licensed under MIT — see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Draft } from './types.js';
|
|
2
|
+
/** Characters of context kept on each side of the selection. */
|
|
3
|
+
export declare const CONTEXT_CHARS = 64;
|
|
4
|
+
/** Reports whether a key event is Ctrl+Enter, or Cmd+Enter on macOS. */
|
|
5
|
+
export declare function isTrigger(event: Pick<KeyboardEvent, 'ctrlKey' | 'metaKey' | 'key'>): boolean;
|
|
6
|
+
/** Reports whether the event target is a field the reader is typing in. */
|
|
7
|
+
export declare function isEditable(target: EventTarget | null): boolean;
|
|
8
|
+
/** Bytes report-relay keeps of one free-text field. */
|
|
9
|
+
export declare const MAX_TEXT_BYTES = 1024;
|
|
10
|
+
/** Cuts text to a UTF-8 byte budget on a code point boundary. */
|
|
11
|
+
export declare function clipBytes(text: string, max?: number): string;
|
|
12
|
+
/** Builds a draft from the text before, inside and after a selection, cut to whole words. */
|
|
13
|
+
export declare function toDraft(before: string, selected: string, after: string, max: number): Draft;
|
|
14
|
+
/** Reads the current selection and its surrounding text from the page. */
|
|
15
|
+
export declare function capture(selection: Selection | null, max: number): Draft | undefined;
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Format } from './transport/webhook.js';
|
|
2
|
+
import type { Messages, Send, Ui } from './types.js';
|
|
3
|
+
/** Options of `reportTypo`; `endpoint` or `send` is required. */
|
|
4
|
+
export interface Options {
|
|
5
|
+
endpoint?: string;
|
|
6
|
+
transport?: 'relay' | 'webhook';
|
|
7
|
+
format?: Format;
|
|
8
|
+
send?: Send;
|
|
9
|
+
locale?: string;
|
|
10
|
+
maxSelection?: number;
|
|
11
|
+
cooldownMs?: number;
|
|
12
|
+
comment?: boolean;
|
|
13
|
+
ui?: Ui;
|
|
14
|
+
}
|
|
15
|
+
/** Picks the catalog entry for a language tag by its base subtag, falling back to the first entry. */
|
|
16
|
+
export declare function pick(catalog: Record<string, Messages>, tag: string): [string, Messages];
|
|
17
|
+
/** Builds `reportTypo` over a locale catalog, so each bundle ships only the locales it imports. */
|
|
18
|
+
export declare function create(catalog: Record<string, Messages>): (options: Options) => () => void;
|
package/dist/dialog.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { Confirmation, Draft, Messages } from './types.js';
|
|
2
|
+
/** Shows the draft in a modal dialog inside a closed shadow root and waits for Send or Cancel. */
|
|
3
|
+
export declare function confirm(draft: Draft, messages: Messages, lang: string, hasComment: boolean): Promise<Confirmation | undefined>;
|
package/dist/en.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
var O={dir:"ltr",title:"Report a typo",send:"Send",cancel:"Cancel",comment:"Comment (optional)",commentPlaceholder:"What should it say?"};var B='input,textarea,select,[contenteditable]:not([contenteditable="false"])',X="p,li,dd,dt,td,th,blockquote,pre,figcaption,h1,h2,h3,h4,h5,h6,article,section,main,body";function R(e){return(e.ctrlKey||e.metaKey)&&e.key==="Enter"}function A(e){return!!e?.closest?.(B)}var E=1024;function N(e,o=E){let t=new TextEncoder,n=e;for(;t.encode(n).length>o;)n=[...n].slice(0,-1).join("");return n}var S=e=>e.replaceAll(/\s+/g," ");function H(e,o,t,n){let r=S(e),c=S(t),i=r.slice(-64),s=c.slice(0,64);return{selection:S(o).trim().slice(0,n),prefix:(r.length>64?i.replace(/^\S*\s/,""):i).trimStart(),suffix:(c.length>64?s.replace(/\s\S*$/,""):s).trimEnd()}}function _(e,o){if(!e||e.isCollapsed||e.rangeCount===0)return;let t=e.getRangeAt(0),n=t.commonAncestorContainer,r=(n instanceof Element?n:n.parentElement)?.closest(X)??document.body,c=document.createRange();c.selectNodeContents(r),c.setEnd(t.startContainer,t.startOffset);let i=document.createRange();i.selectNodeContents(r),i.setStart(t.endContainer,t.endOffset);let s=H(c.toString(),t.toString(),i.toString(),o);return s.selection?s:void 0}var F=`dialog{max-inline-size:min(32rem,90vw);font:var(--report-typo-font,1rem/1.5 system-ui,sans-serif);
|
|
2
|
+
color:var(--report-typo-color,CanvasText);background:var(--report-typo-background,Canvas);
|
|
3
|
+
border:var(--report-typo-border,1px solid GrayText);border-radius:var(--report-typo-radius,.5rem)}
|
|
4
|
+
p,label{display:block;margin:0 0 1rem}mark{padding:0 .1em;background:var(--report-typo-highlight,Mark)}
|
|
5
|
+
textarea{display:block;box-sizing:border-box;inline-size:100%;font:inherit}
|
|
6
|
+
menu{display:flex;gap:.5rem;justify-content:end;margin:0;padding:0}`,l=(e,o,t="")=>{let n=document.createElement(e);return n.textContent=t,o.append(n),n};function j(e,o,t,n){let r=document.createElement("report-typo"),c=r.attachShadow({mode:"closed"}),i=new CSSStyleSheet;i.replaceSync(F),c.adoptedStyleSheets=[i];let s=l("dialog",c);s.dir=o.dir,s.lang=t;let m=l("form",s);m.method="dialog",l("h2",m,o.title);let y=l("p",m,e.prefix);l("mark",y,e.selection),y.append(e.suffix);let f=n?l("label",m,o.comment):void 0,a=f?l("textarea",f):void 0;a&&(a.placeholder=o.commentPlaceholder,a.maxLength=E,a.rows=3);let g=l("menu",m);l("button",g,o.cancel).value="cancel";let b=l("button",g,o.send);return b.value="send",b.autofocus=!a,document.body.append(r),new Promise(h=>{s.addEventListener("close",()=>{r.remove(),h(s.returnValue==="send"?{comment:a?.value}:void 0)}),s.showModal(),a?.focus()})}var K=(e,o)=>fetch(e,{method:"POST",headers:{"Content-Type":"application/reports+json"},body:JSON.stringify([o]),keepalive:!0,credentials:"omit",mode:"cors",signal:AbortSignal.timeout(5e3)});function U(e,o=Math.random){let t=Number(e);return(Number.isFinite(t)&&t>0?t:1)*1e3*(1+o()/2)}function C(e){return async o=>{let t=await K(e,o);if(t.status===429){let n=U(t.headers.get("Retry-After"));console.debug("report-typo: relay busy, retrying",{status:t.status,wait:n}),await new Promise(r=>setTimeout(r,n)),t=await K(e,o)}if(!t.ok)throw new Error(`report-relay answered ${t.status}`)}}function k(e,o=t=>t){return async t=>{let n=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o(t)),keepalive:!0,credentials:"omit",signal:AbortSignal.timeout(5e3)});if(!n.ok)throw new Error(`webhook answered ${n.status}`)}}var p=(e,o)=>console.debug(`report-typo: ${e}`,o);function I(e,o){let t=o.toLowerCase().split("-",1)[0]??"",n=e[t];if(n)return[t,n];let[r]=Object.entries(e);if(!r)throw new TypeError("report-typo: empty locale catalog");return r}function P(e){return function(t){let{endpoint:n,transport:r="relay",format:c,maxSelection:i=256,cooldownMs:s=1e4,comment:m=!1}=t;if(!n&&!t.send)throw new TypeError("report-typo: set `endpoint` or `send`");let y=t.send??(r==="webhook"?k(n,c):C(n)),f=document.documentElement.lang,[a,g]=I(e,t.locale??(f||navigator.language)),b=t.ui??(d=>j(d,g,a,m)),h=-1/0,w=!1,D=async d=>{if(!R(d))return;if(A(d.target))return p("ignored in an editable field",{target:d.target});let u=_(getSelection(),i);if(!u)return p("ignored without a selection",{maxSelection:i});let M=h+s-Date.now();if(M>0)return p("ignored while cooling down",{wait:M});if(w)return p("ignored while the dialog is open",{selection:u.selection});d.preventDefault(),w=!0;try{let T=await b(u);if(!T)return p("cancelled",{selection:u.selection});h=Date.now();let x=N(T.comment?.trim()??""),L={...u,lang:f||a,...x&&{comment:x}};await y({type:"typo",age:0,url:location.href,body:L}),p("sent",{selection:u.selection,transport:r,comment:!!x})}catch(T){p("not sent",{error:T})}finally{w=!1}},v=d=>{D(d)};return document.addEventListener("keydown",v),p("listening",{locale:a,transport:r,cooldownMs:s}),()=>{document.removeEventListener("keydown",v),p("disposed",{locale:a})}}}var re=P({en:O});export{C as relay,re as reportTypo,k as webhook};
|
|
7
|
+
//# sourceMappingURL=en.js.map
|
package/dist/en.js.map
ADDED
|
@@ -0,0 +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/** 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 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","// 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}\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/** 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 = 10_000,\n comment = false\n } = options\n if (!endpoint && !options.send) throw new TypeError('report-typo: set `endpoint` or `send`')\n const send =\n options.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: location.href, 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","capture","selection","range","common","scope","draft","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","create","options","endpoint","transport","format","maxSelection","cooldownMs","comment","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"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { Options } from '../core.js';
|
|
2
|
+
export type { Confirmation, Draft, Messages, Send, TypoBody, TypoReport, Ui } from '../types.js';
|
|
3
|
+
export { relay } from '../transport/relay.js';
|
|
4
|
+
export { webhook, type Format } from '../transport/webhook.js';
|
|
5
|
+
/** Starts listening for Ctrl+Enter and returns the function that stops it. */
|
|
6
|
+
export declare const reportTypo: (options: import("../core.js").Options) => () => void;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { Options } from '../core.js';
|
|
2
|
+
export type { Confirmation, Draft, Messages, Send, TypoBody, TypoReport, Ui } from '../types.js';
|
|
3
|
+
export { relay } from '../transport/relay.js';
|
|
4
|
+
export { webhook, type Format } from '../transport/webhook.js';
|
|
5
|
+
/** Starts listening for Ctrl+Enter and returns the function that stops it. */
|
|
6
|
+
export declare const reportTypo: (options: import("../core.js").Options) => () => void;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { Options } from '../core.js';
|
|
2
|
+
export type { Confirmation, Draft, Messages, Send, TypoBody, TypoReport, Ui } from '../types.js';
|
|
3
|
+
export { relay } from '../transport/relay.js';
|
|
4
|
+
export { webhook, type Format } from '../transport/webhook.js';
|
|
5
|
+
/** Starts listening for Ctrl+Enter and returns the function that stops it. */
|
|
6
|
+
export declare const reportTypo: (options: import("../core.js").Options) => () => void;
|
package/dist/es.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
var O={dir:"ltr",title:"Avisar de una errata",send:"Enviar",cancel:"Cancelar",comment:"Comentario (opcional)",commentPlaceholder:"\xBFQu\xE9 deber\xEDa decir?"};var B='input,textarea,select,[contenteditable]:not([contenteditable="false"])',X="p,li,dd,dt,td,th,blockquote,pre,figcaption,h1,h2,h3,h4,h5,h6,article,section,main,body";function R(e){return(e.ctrlKey||e.metaKey)&&e.key==="Enter"}function A(e){return!!e?.closest?.(B)}var E=1024;function N(e,o=E){let t=new TextEncoder,n=e;for(;t.encode(n).length>o;)n=[...n].slice(0,-1).join("");return n}var S=e=>e.replaceAll(/\s+/g," ");function H(e,o,t,n){let r=S(e),c=S(t),s=r.slice(-64),i=c.slice(0,64);return{selection:S(o).trim().slice(0,n),prefix:(r.length>64?s.replace(/^\S*\s/,""):s).trimStart(),suffix:(c.length>64?i.replace(/\s\S*$/,""):i).trimEnd()}}function _(e,o){if(!e||e.isCollapsed||e.rangeCount===0)return;let t=e.getRangeAt(0),n=t.commonAncestorContainer,r=(n instanceof Element?n:n.parentElement)?.closest(X)??document.body,c=document.createRange();c.selectNodeContents(r),c.setEnd(t.startContainer,t.startOffset);let s=document.createRange();s.selectNodeContents(r),s.setStart(t.endContainer,t.endOffset);let i=H(c.toString(),t.toString(),s.toString(),o);return i.selection?i:void 0}var F=`dialog{max-inline-size:min(32rem,90vw);font:var(--report-typo-font,1rem/1.5 system-ui,sans-serif);
|
|
2
|
+
color:var(--report-typo-color,CanvasText);background:var(--report-typo-background,Canvas);
|
|
3
|
+
border:var(--report-typo-border,1px solid GrayText);border-radius:var(--report-typo-radius,.5rem)}
|
|
4
|
+
p,label{display:block;margin:0 0 1rem}mark{padding:0 .1em;background:var(--report-typo-highlight,Mark)}
|
|
5
|
+
textarea{display:block;box-sizing:border-box;inline-size:100%;font:inherit}
|
|
6
|
+
menu{display:flex;gap:.5rem;justify-content:end;margin:0;padding:0}`,l=(e,o,t="")=>{let n=document.createElement(e);return n.textContent=t,o.append(n),n};function j(e,o,t,n){let r=document.createElement("report-typo"),c=r.attachShadow({mode:"closed"}),s=new CSSStyleSheet;s.replaceSync(F),c.adoptedStyleSheets=[s];let i=l("dialog",c);i.dir=o.dir,i.lang=t;let m=l("form",i);m.method="dialog",l("h2",m,o.title);let y=l("p",m,e.prefix);l("mark",y,e.selection),y.append(e.suffix);let f=n?l("label",m,o.comment):void 0,a=f?l("textarea",f):void 0;a&&(a.placeholder=o.commentPlaceholder,a.maxLength=E,a.rows=3);let g=l("menu",m);l("button",g,o.cancel).value="cancel";let b=l("button",g,o.send);return b.value="send",b.autofocus=!a,document.body.append(r),new Promise(h=>{i.addEventListener("close",()=>{r.remove(),h(i.returnValue==="send"?{comment:a?.value}:void 0)}),i.showModal(),a?.focus()})}var K=(e,o)=>fetch(e,{method:"POST",headers:{"Content-Type":"application/reports+json"},body:JSON.stringify([o]),keepalive:!0,credentials:"omit",mode:"cors",signal:AbortSignal.timeout(5e3)});function U(e,o=Math.random){let t=Number(e);return(Number.isFinite(t)&&t>0?t:1)*1e3*(1+o()/2)}function C(e){return async o=>{let t=await K(e,o);if(t.status===429){let n=U(t.headers.get("Retry-After"));console.debug("report-typo: relay busy, retrying",{status:t.status,wait:n}),await new Promise(r=>setTimeout(r,n)),t=await K(e,o)}if(!t.ok)throw new Error(`report-relay answered ${t.status}`)}}function k(e,o=t=>t){return async t=>{let n=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o(t)),keepalive:!0,credentials:"omit",signal:AbortSignal.timeout(5e3)});if(!n.ok)throw new Error(`webhook answered ${n.status}`)}}var d=(e,o)=>console.debug(`report-typo: ${e}`,o);function I(e,o){let t=o.toLowerCase().split("-",1)[0]??"",n=e[t];if(n)return[t,n];let[r]=Object.entries(e);if(!r)throw new TypeError("report-typo: empty locale catalog");return r}function P(e){return function(t){let{endpoint:n,transport:r="relay",format:c,maxSelection:s=256,cooldownMs:i=1e4,comment:m=!1}=t;if(!n&&!t.send)throw new TypeError("report-typo: set `endpoint` or `send`");let y=t.send??(r==="webhook"?k(n,c):C(n)),f=document.documentElement.lang,[a,g]=I(e,t.locale??(f||navigator.language)),b=t.ui??(p=>j(p,g,a,m)),h=-1/0,w=!1,D=async p=>{if(!R(p))return;if(A(p.target))return d("ignored in an editable field",{target:p.target});let u=_(getSelection(),s);if(!u)return d("ignored without a selection",{maxSelection:s});let M=h+i-Date.now();if(M>0)return d("ignored while cooling down",{wait:M});if(w)return d("ignored while the dialog is open",{selection:u.selection});p.preventDefault(),w=!0;try{let T=await b(u);if(!T)return d("cancelled",{selection:u.selection});h=Date.now();let x=N(T.comment?.trim()??""),L={...u,lang:f||a,...x&&{comment:x}};await y({type:"typo",age:0,url:location.href,body:L}),d("sent",{selection:u.selection,transport:r,comment:!!x})}catch(T){d("not sent",{error:T})}finally{w=!1}},v=p=>{D(p)};return document.addEventListener("keydown",v),d("listening",{locale:a,transport:r,cooldownMs:i}),()=>{document.removeEventListener("keydown",v),d("disposed",{locale:a})}}}var re=P({es:O});export{C as relay,re as reportTypo,k as webhook};
|
|
7
|
+
//# sourceMappingURL=es.js.map
|
package/dist/es.js.map
ADDED
|
@@ -0,0 +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/** 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 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","// 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}\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/** 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 = 10_000,\n comment = false\n } = options\n if (!endpoint && !options.send) throw new TypeError('report-typo: set `endpoint` or `send`')\n const send =\n options.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: location.href, 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","capture","selection","range","common","scope","draft","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","create","options","endpoint","transport","format","maxSelection","cooldownMs","comment","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.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { Options } from './core.js';
|
|
2
|
+
export type { Confirmation, Draft, Messages, Send, TypoBody, TypoReport, Ui } from './types.js';
|
|
3
|
+
export { relay } from './transport/relay.js';
|
|
4
|
+
export { webhook, type Format } from './transport/webhook.js';
|
|
5
|
+
/** Starts listening for Ctrl+Enter and returns the function that stops it. */
|
|
6
|
+
export declare const reportTypo: (options: import("./core.js").Options) => () => void;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
var O={dir:"ltr",title:"Report a typo",send:"Send",cancel:"Cancel",comment:"Comment (optional)",commentPlaceholder:"What should it say?"};var R={dir:"ltr",title:"Avisar de una errata",send:"Enviar",cancel:"Cancelar",comment:"Comentario (opcional)",commentPlaceholder:"\xBFQu\xE9 deber\xEDa decir?"};var A={dir:"ltr",title:"\u041F\u043E\u0432\u0456\u0434\u043E\u043C\u0438\u0442\u0438 \u043F\u0440\u043E \u043E\u0434\u0440\u0443\u043A\u0456\u0432\u043A\u0443",send:"\u041D\u0430\u0434\u0456\u0441\u043B\u0430\u0442\u0438",cancel:"\u0421\u043A\u0430\u0441\u0443\u0432\u0430\u0442\u0438",comment:"\u041A\u043E\u043C\u0435\u043D\u0442\u0430\u0440 (\u043D\u0435\u043E\u0431\u043E\u0432\u2019\u044F\u0437\u043A\u043E\u0432\u043E)",commentPlaceholder:"\u042F\u043A \u043C\u0430\u0454 \u0431\u0443\u0442\u0438?"};var H='input,textarea,select,[contenteditable]:not([contenteditable="false"])',F="p,li,dd,dt,td,th,blockquote,pre,figcaption,h1,h2,h3,h4,h5,h6,article,section,main,body";function N(e){return(e.ctrlKey||e.metaKey)&&e.key==="Enter"}function P(e){return!!e?.closest?.(H)}var E=1024;function _(e,o=E){let t=new TextEncoder,n=e;for(;t.encode(n).length>o;)n=[...n].slice(0,-1).join("");return n}var S=e=>e.replaceAll(/\s+/g," ");function U(e,o,t,n){let r=S(e),c=S(t),s=r.slice(-64),i=c.slice(0,64);return{selection:S(o).trim().slice(0,n),prefix:(r.length>64?s.replace(/^\S*\s/,""):s).trimStart(),suffix:(c.length>64?i.replace(/\s\S*$/,""):i).trimEnd()}}function j(e,o){if(!e||e.isCollapsed||e.rangeCount===0)return;let t=e.getRangeAt(0),n=t.commonAncestorContainer,r=(n instanceof Element?n:n.parentElement)?.closest(F)??document.body,c=document.createRange();c.selectNodeContents(r),c.setEnd(t.startContainer,t.startOffset);let s=document.createRange();s.selectNodeContents(r),s.setStart(t.endContainer,t.endOffset);let i=U(c.toString(),t.toString(),s.toString(),o);return i.selection?i:void 0}var I=`dialog{max-inline-size:min(32rem,90vw);font:var(--report-typo-font,1rem/1.5 system-ui,sans-serif);
|
|
2
|
+
color:var(--report-typo-color,CanvasText);background:var(--report-typo-background,Canvas);
|
|
3
|
+
border:var(--report-typo-border,1px solid GrayText);border-radius:var(--report-typo-radius,.5rem)}
|
|
4
|
+
p,label{display:block;margin:0 0 1rem}mark{padding:0 .1em;background:var(--report-typo-highlight,Mark)}
|
|
5
|
+
textarea{display:block;box-sizing:border-box;inline-size:100%;font:inherit}
|
|
6
|
+
menu{display:flex;gap:.5rem;justify-content:end;margin:0;padding:0}`,l=(e,o,t="")=>{let n=document.createElement(e);return n.textContent=t,o.append(n),n};function K(e,o,t,n){let r=document.createElement("report-typo"),c=r.attachShadow({mode:"closed"}),s=new CSSStyleSheet;s.replaceSync(I),c.adoptedStyleSheets=[s];let i=l("dialog",c);i.dir=o.dir,i.lang=t;let m=l("form",i);m.method="dialog",l("h2",m,o.title);let y=l("p",m,e.prefix);l("mark",y,e.selection),y.append(e.suffix);let f=n?l("label",m,o.comment):void 0,a=f?l("textarea",f):void 0;a&&(a.placeholder=o.commentPlaceholder,a.maxLength=E,a.rows=3);let g=l("menu",m);l("button",g,o.cancel).value="cancel";let b=l("button",g,o.send);return b.value="send",b.autofocus=!a,document.body.append(r),new Promise(h=>{i.addEventListener("close",()=>{r.remove(),h(i.returnValue==="send"?{comment:a?.value}:void 0)}),i.showModal(),a?.focus()})}var D=(e,o)=>fetch(e,{method:"POST",headers:{"Content-Type":"application/reports+json"},body:JSON.stringify([o]),keepalive:!0,credentials:"omit",mode:"cors",signal:AbortSignal.timeout(5e3)});function $(e,o=Math.random){let t=Number(e);return(Number.isFinite(t)&&t>0?t:1)*1e3*(1+o()/2)}function C(e){return async o=>{let t=await D(e,o);if(t.status===429){let n=$(t.headers.get("Retry-After"));console.debug("report-typo: relay busy, retrying",{status:t.status,wait:n}),await new Promise(r=>setTimeout(r,n)),t=await D(e,o)}if(!t.ok)throw new Error(`report-relay answered ${t.status}`)}}function k(e,o=t=>t){return async t=>{let n=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o(t)),keepalive:!0,credentials:"omit",signal:AbortSignal.timeout(5e3)});if(!n.ok)throw new Error(`webhook answered ${n.status}`)}}var d=(e,o)=>console.debug(`report-typo: ${e}`,o);function z(e,o){let t=o.toLowerCase().split("-",1)[0]??"",n=e[t];if(n)return[t,n];let[r]=Object.entries(e);if(!r)throw new TypeError("report-typo: empty locale catalog");return r}function L(e){return function(t){let{endpoint:n,transport:r="relay",format:c,maxSelection:s=256,cooldownMs:i=1e4,comment:m=!1}=t;if(!n&&!t.send)throw new TypeError("report-typo: set `endpoint` or `send`");let y=t.send??(r==="webhook"?k(n,c):C(n)),f=document.documentElement.lang,[a,g]=z(e,t.locale??(f||navigator.language)),b=t.ui??(p=>K(p,g,a,m)),h=-1/0,x=!1,B=async p=>{if(!N(p))return;if(P(p.target))return d("ignored in an editable field",{target:p.target});let u=j(getSelection(),s);if(!u)return d("ignored without a selection",{maxSelection:s});let M=h+i-Date.now();if(M>0)return d("ignored while cooling down",{wait:M});if(x)return d("ignored while the dialog is open",{selection:u.selection});p.preventDefault(),x=!0;try{let T=await b(u);if(!T)return d("cancelled",{selection:u.selection});h=Date.now();let w=_(T.comment?.trim()??""),X={...u,lang:f||a,...w&&{comment:w}};await y({type:"typo",age:0,url:location.href,body:X}),d("sent",{selection:u.selection,transport:r,comment:!!w})}catch(T){d("not sent",{error:T})}finally{x=!1}},v=p=>{B(p)};return document.addEventListener("keydown",v),d("listening",{locale:a,transport:r,cooldownMs:i}),()=>{document.removeEventListener("keydown",v),d("disposed",{locale:a})}}}var de=L({en:O,es:R,uk:A});export{C as relay,de as reportTypo,k as webhook};
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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/** 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 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","// 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}\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/** 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 = 10_000,\n comment = false\n } = options\n if (!endpoint && !options.send) throw new TypeError('report-typo: set `endpoint` or `send`')\n const send =\n options.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: location.href, 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","capture","selection","range","common","scope","draft","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","create","options","endpoint","transport","format","maxSelection","cooldownMs","comment","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"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Send } from '../types.js';
|
|
2
|
+
/** Per-attempt deadline of one delivery. */
|
|
3
|
+
export declare const TIMEOUT_MS = 5000;
|
|
4
|
+
/** Milliseconds to wait on a `Retry-After` header, with up to 50% jitter; 1 s when absent or a date. */
|
|
5
|
+
export declare function retryAfterMs(header: string | null, random?: () => number): number;
|
|
6
|
+
/** Sends reports to a report-relay intake, retrying once on 429. */
|
|
7
|
+
export declare function relay(endpoint: string): Send;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Send, TypoReport } from '../types.js';
|
|
2
|
+
/** Turns a report into the JSON a webhook expects. */
|
|
3
|
+
export type Format = (report: TypoReport) => unknown;
|
|
4
|
+
/** Sends reports as a plain JSON POST, shaped by `format` when given. */
|
|
5
|
+
export declare function webhook(url: string, format?: Format): Send;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** The text a reader selected, with the words around it. */
|
|
2
|
+
export interface Draft {
|
|
3
|
+
selection: string;
|
|
4
|
+
prefix: string;
|
|
5
|
+
suffix: string;
|
|
6
|
+
}
|
|
7
|
+
/** The body of a `typo` report, as report-relay accepts it. */
|
|
8
|
+
export interface TypoBody extends Draft {
|
|
9
|
+
lang: string;
|
|
10
|
+
comment?: string;
|
|
11
|
+
}
|
|
12
|
+
/** One report in the Reporting API envelope. */
|
|
13
|
+
export interface TypoReport {
|
|
14
|
+
type: 'typo';
|
|
15
|
+
age: number;
|
|
16
|
+
url: string;
|
|
17
|
+
body: TypoBody;
|
|
18
|
+
}
|
|
19
|
+
/** What the reader confirmed. */
|
|
20
|
+
export interface Confirmation {
|
|
21
|
+
comment?: string;
|
|
22
|
+
}
|
|
23
|
+
/** Shows a draft to the reader; resolves null or undefined when they cancel. */
|
|
24
|
+
export type Ui = (draft: Draft) => Promise<Confirmation | null | undefined>;
|
|
25
|
+
/** Delivers one report; rejects when it was not delivered. */
|
|
26
|
+
export type Send = (report: TypoReport) => Promise<void>;
|
|
27
|
+
/** The strings of one locale. */
|
|
28
|
+
export interface Messages {
|
|
29
|
+
dir: 'ltr' | 'rtl';
|
|
30
|
+
title: string;
|
|
31
|
+
send: string;
|
|
32
|
+
cancel: string;
|
|
33
|
+
comment: string;
|
|
34
|
+
commentPlaceholder: string;
|
|
35
|
+
}
|
package/dist/uk.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
var O={dir:"ltr",title:"\u041F\u043E\u0432\u0456\u0434\u043E\u043C\u0438\u0442\u0438 \u043F\u0440\u043E \u043E\u0434\u0440\u0443\u043A\u0456\u0432\u043A\u0443",send:"\u041D\u0430\u0434\u0456\u0441\u043B\u0430\u0442\u0438",cancel:"\u0421\u043A\u0430\u0441\u0443\u0432\u0430\u0442\u0438",comment:"\u041A\u043E\u043C\u0435\u043D\u0442\u0430\u0440 (\u043D\u0435\u043E\u0431\u043E\u0432\u2019\u044F\u0437\u043A\u043E\u0432\u043E)",commentPlaceholder:"\u042F\u043A \u043C\u0430\u0454 \u0431\u0443\u0442\u0438?"};var B='input,textarea,select,[contenteditable]:not([contenteditable="false"])',X="p,li,dd,dt,td,th,blockquote,pre,figcaption,h1,h2,h3,h4,h5,h6,article,section,main,body";function R(e){return(e.ctrlKey||e.metaKey)&&e.key==="Enter"}function A(e){return!!e?.closest?.(B)}var E=1024;function N(e,o=E){let t=new TextEncoder,n=e;for(;t.encode(n).length>o;)n=[...n].slice(0,-1).join("");return n}var S=e=>e.replaceAll(/\s+/g," ");function H(e,o,t,n){let r=S(e),c=S(t),i=r.slice(-64),s=c.slice(0,64);return{selection:S(o).trim().slice(0,n),prefix:(r.length>64?i.replace(/^\S*\s/,""):i).trimStart(),suffix:(c.length>64?s.replace(/\s\S*$/,""):s).trimEnd()}}function _(e,o){if(!e||e.isCollapsed||e.rangeCount===0)return;let t=e.getRangeAt(0),n=t.commonAncestorContainer,r=(n instanceof Element?n:n.parentElement)?.closest(X)??document.body,c=document.createRange();c.selectNodeContents(r),c.setEnd(t.startContainer,t.startOffset);let i=document.createRange();i.selectNodeContents(r),i.setStart(t.endContainer,t.endOffset);let s=H(c.toString(),t.toString(),i.toString(),o);return s.selection?s:void 0}var F=`dialog{max-inline-size:min(32rem,90vw);font:var(--report-typo-font,1rem/1.5 system-ui,sans-serif);
|
|
2
|
+
color:var(--report-typo-color,CanvasText);background:var(--report-typo-background,Canvas);
|
|
3
|
+
border:var(--report-typo-border,1px solid GrayText);border-radius:var(--report-typo-radius,.5rem)}
|
|
4
|
+
p,label{display:block;margin:0 0 1rem}mark{padding:0 .1em;background:var(--report-typo-highlight,Mark)}
|
|
5
|
+
textarea{display:block;box-sizing:border-box;inline-size:100%;font:inherit}
|
|
6
|
+
menu{display:flex;gap:.5rem;justify-content:end;margin:0;padding:0}`,l=(e,o,t="")=>{let n=document.createElement(e);return n.textContent=t,o.append(n),n};function j(e,o,t,n){let r=document.createElement("report-typo"),c=r.attachShadow({mode:"closed"}),i=new CSSStyleSheet;i.replaceSync(F),c.adoptedStyleSheets=[i];let s=l("dialog",c);s.dir=o.dir,s.lang=t;let m=l("form",s);m.method="dialog",l("h2",m,o.title);let y=l("p",m,e.prefix);l("mark",y,e.selection),y.append(e.suffix);let f=n?l("label",m,o.comment):void 0,a=f?l("textarea",f):void 0;a&&(a.placeholder=o.commentPlaceholder,a.maxLength=E,a.rows=3);let g=l("menu",m);l("button",g,o.cancel).value="cancel";let b=l("button",g,o.send);return b.value="send",b.autofocus=!a,document.body.append(r),new Promise(h=>{s.addEventListener("close",()=>{r.remove(),h(s.returnValue==="send"?{comment:a?.value}:void 0)}),s.showModal(),a?.focus()})}var K=(e,o)=>fetch(e,{method:"POST",headers:{"Content-Type":"application/reports+json"},body:JSON.stringify([o]),keepalive:!0,credentials:"omit",mode:"cors",signal:AbortSignal.timeout(5e3)});function U(e,o=Math.random){let t=Number(e);return(Number.isFinite(t)&&t>0?t:1)*1e3*(1+o()/2)}function k(e){return async o=>{let t=await K(e,o);if(t.status===429){let n=U(t.headers.get("Retry-After"));console.debug("report-typo: relay busy, retrying",{status:t.status,wait:n}),await new Promise(r=>setTimeout(r,n)),t=await K(e,o)}if(!t.ok)throw new Error(`report-relay answered ${t.status}`)}}function C(e,o=t=>t){return async t=>{let n=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o(t)),keepalive:!0,credentials:"omit",signal:AbortSignal.timeout(5e3)});if(!n.ok)throw new Error(`webhook answered ${n.status}`)}}var p=(e,o)=>console.debug(`report-typo: ${e}`,o);function I(e,o){let t=o.toLowerCase().split("-",1)[0]??"",n=e[t];if(n)return[t,n];let[r]=Object.entries(e);if(!r)throw new TypeError("report-typo: empty locale catalog");return r}function P(e){return function(t){let{endpoint:n,transport:r="relay",format:c,maxSelection:i=256,cooldownMs:s=1e4,comment:m=!1}=t;if(!n&&!t.send)throw new TypeError("report-typo: set `endpoint` or `send`");let y=t.send??(r==="webhook"?C(n,c):k(n)),f=document.documentElement.lang,[a,g]=I(e,t.locale??(f||navigator.language)),b=t.ui??(d=>j(d,g,a,m)),h=-1/0,w=!1,D=async d=>{if(!R(d))return;if(A(d.target))return p("ignored in an editable field",{target:d.target});let u=_(getSelection(),i);if(!u)return p("ignored without a selection",{maxSelection:i});let M=h+s-Date.now();if(M>0)return p("ignored while cooling down",{wait:M});if(w)return p("ignored while the dialog is open",{selection:u.selection});d.preventDefault(),w=!0;try{let T=await b(u);if(!T)return p("cancelled",{selection:u.selection});h=Date.now();let x=N(T.comment?.trim()??""),L={...u,lang:f||a,...x&&{comment:x}};await y({type:"typo",age:0,url:location.href,body:L}),p("sent",{selection:u.selection,transport:r,comment:!!x})}catch(T){p("not sent",{error:T})}finally{w=!1}},v=d=>{D(d)};return document.addEventListener("keydown",v),p("listening",{locale:a,transport:r,cooldownMs:s}),()=>{document.removeEventListener("keydown",v),p("disposed",{locale:a})}}}var re=P({uk:O});export{k as relay,re as reportTypo,C as webhook};
|
|
7
|
+
//# sourceMappingURL=uk.js.map
|
package/dist/uk.js.map
ADDED
|
@@ -0,0 +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/** 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 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","// 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}\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/** 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 = 10_000,\n comment = false\n } = options\n if (!endpoint && !options.send) throw new TypeError('report-typo: set `endpoint` or `send`')\n const send =\n options.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: location.href, 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","capture","selection","range","common","scope","draft","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","create","options","endpoint","transport","format","maxSelection","cooldownMs","comment","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,121 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "report-typo",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Ctrl+Enter typo reporting for any website",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./en": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/en.js"
|
|
16
|
+
},
|
|
17
|
+
"./es": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/es.js"
|
|
20
|
+
},
|
|
21
|
+
"./uk": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/uk.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist/"
|
|
28
|
+
],
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=24"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"prebuild": "rm -rf dist",
|
|
35
|
+
"build": "tsup && tsc --project tsconfig.build.json",
|
|
36
|
+
"build:test": "TEST_BUILD=1 tsup",
|
|
37
|
+
"pretest": "npm run build:test",
|
|
38
|
+
"test": "node --test 'dist/test/*.test.js'",
|
|
39
|
+
"posttest": "npm run build && size-limit",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"check-size": "size-limit",
|
|
42
|
+
"prepublishOnly": "npm run build",
|
|
43
|
+
"lint": "eslint ./",
|
|
44
|
+
"format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts'",
|
|
45
|
+
"format:check": "prettier --check 'src/**/*.ts' 'tests/**/*.ts'"
|
|
46
|
+
},
|
|
47
|
+
"keywords": [
|
|
48
|
+
"typo",
|
|
49
|
+
"orphus",
|
|
50
|
+
"proofreading",
|
|
51
|
+
"reporting-api",
|
|
52
|
+
"browser"
|
|
53
|
+
],
|
|
54
|
+
"author": {
|
|
55
|
+
"email": "damian.buho@proton.me",
|
|
56
|
+
"name": "Damián Búho",
|
|
57
|
+
"url": "https://dbuho.me"
|
|
58
|
+
},
|
|
59
|
+
"license": "MIT",
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public"
|
|
62
|
+
},
|
|
63
|
+
"repository": {
|
|
64
|
+
"type": "git",
|
|
65
|
+
"url": "git+https://github.com/damian-buho/report-typo.git"
|
|
66
|
+
},
|
|
67
|
+
"bugs": {
|
|
68
|
+
"url": "https://github.com/damian-buho/report-typo/issues"
|
|
69
|
+
},
|
|
70
|
+
"homepage": "https://github.com/damian-buho/report-typo#readme",
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@eslint/js": "10.0.1",
|
|
73
|
+
"@size-limit/preset-small-lib": "14.2.0",
|
|
74
|
+
"@types/eslint": "9.6.1",
|
|
75
|
+
"@types/node": "26.6.5",
|
|
76
|
+
"esbuild": "0.28.2",
|
|
77
|
+
"eslint": "10.12.0",
|
|
78
|
+
"eslint-config-prettier": "10.1.8",
|
|
79
|
+
"eslint-plugin-import-x": "4.17.1",
|
|
80
|
+
"eslint-plugin-n": "18.4.1",
|
|
81
|
+
"eslint-plugin-promise": "7.3.0",
|
|
82
|
+
"eslint-plugin-tsdoc": "0.5.4",
|
|
83
|
+
"eslint-plugin-unicorn": "78.0.0",
|
|
84
|
+
"globals": "17.13.0",
|
|
85
|
+
"prettier": "3.9.10",
|
|
86
|
+
"size-limit": "14.2.0",
|
|
87
|
+
"tsup": "8.5.1",
|
|
88
|
+
"typescript": "6.0.3",
|
|
89
|
+
"typescript-eslint": "8.71.2",
|
|
90
|
+
"yaml": "2.9.1"
|
|
91
|
+
},
|
|
92
|
+
"prettier": {
|
|
93
|
+
"arrowParens": "avoid",
|
|
94
|
+
"bracketSpacing": true,
|
|
95
|
+
"printWidth": 100,
|
|
96
|
+
"quoteProps": "as-needed",
|
|
97
|
+
"semi": false,
|
|
98
|
+
"singleQuote": true,
|
|
99
|
+
"tabWidth": 2,
|
|
100
|
+
"trailingComma": "none",
|
|
101
|
+
"useTabs": false
|
|
102
|
+
},
|
|
103
|
+
"overrides": {
|
|
104
|
+
"esbuild": "0.28.2",
|
|
105
|
+
"globals": "$globals"
|
|
106
|
+
},
|
|
107
|
+
"size-limit": [
|
|
108
|
+
{
|
|
109
|
+
"path": "./dist/index.js",
|
|
110
|
+
"limit": "3 kB",
|
|
111
|
+
"gzip": true
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
"maintainers": [
|
|
115
|
+
{
|
|
116
|
+
"email": "damian.buho@proton.me",
|
|
117
|
+
"name": "Damián Búho",
|
|
118
|
+
"url": "https://dbuho.me"
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
}
|