fewrd 0.2.0 → 0.3.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/README.md CHANGED
@@ -12,7 +12,7 @@ few words.**
12
12
  ![dependencies: 0](https://img.shields.io/badge/dependencies-0-78C4B6?style=flat-square)
13
13
  ![core: 4 kB gzip](https://img.shields.io/badge/core-4%20kB%20gzip-78C4B6?style=flat-square)
14
14
  ![types: strict](https://img.shields.io/badge/types-strict-78C4B6?style=flat-square)
15
- ![tests: 52 passing](https://img.shields.io/badge/tests-52%20passing-78C4B6?style=flat-square)
15
+ ![tests: 59 passing](https://img.shields.io/badge/tests-59%20passing-78C4B6?style=flat-square)
16
16
  [![license: MIT](https://img.shields.io/badge/license-MIT-78C4B6?style=flat-square)](LICENSE)
17
17
 
18
18
  </div>
@@ -205,8 +205,31 @@ nest. Toggle between the full and condensed view with one class:
205
205
 
206
206
  ## Playground
207
207
 
208
- Try your own book in the browser — the same playground this repo uses, with
209
- your recipes and your cases:
208
+ Keep a book in a folder next to your code, and open it in the playground with
209
+ one command. `fewrd-play` is a separate dev-only package, so none of it ends up
210
+ in the library your code ships:
211
+
212
+ ```bash
213
+ npm install -D fewrd-play
214
+ ```
215
+ ```bash
216
+ npx fewrd-play path/to/book --open
217
+ ```
218
+
219
+ ```
220
+ path/to/book/
221
+ book.json the recipes; the playground's save button writes it back
222
+ cases.json sample texts: [{ "name": "refund", "text": "Refund approved - SKU: abc-1234" }]
223
+ cases.local.json more texts, e.g. real ones you keep out of git (optional)
224
+ resolvers.ts export const resolvers = { upper: (p) => p.value.toUpperCase() } (optional)
225
+ ```
226
+
227
+ `resolvers.ts` is served with its types stripped (Node 22.13+), so it may
228
+ import types, `fewrd` itself and other files in the folder, but no other
229
+ packages. A `resolvers.js` is served as is. The server listens on localhost
230
+ only; `--port` picks the port (default 4747).
231
+
232
+ Or mount the playground in a page of your own:
210
233
 
211
234
  ```ts
212
235
  import { mount } from 'fewrd/playground';
@@ -217,14 +240,15 @@ mount(document.getElementById('app')!, {
217
240
  resolvers, // your named resolvers
218
241
  cases: [{ name: 'refund', text: 'Refund approved - SKU: abc-1234 - customer notified' }],
219
242
  fold: (m) => m.entity === 'sku', // optional: which entities start folded
243
+ save: (json) => fetch('/book', { method: 'POST', body: json }), // optional: a save button
220
244
  });
221
245
  ```
222
246
 
223
247
  Every edit recompiles the book and re-reads every case; invalid JSON keeps the
224
- last good result on screen, and reset brings the original back. Pass
248
+ last good result on screen, and reset brings back the last saved book. Pass
225
249
  `book` instead of `data`/`resolvers` for a read-only view of a compiled
226
250
  `Book`. The playground injects its own scoped styles, so it needs no
227
- stylesheet; serve it with whatever dev server you already use.
251
+ stylesheet.
228
252
 
229
253
  ## Demo books
230
254
 
@@ -1,10 +1,10 @@
1
- import{read as E,compile as O,html as H,gist as J}from"./index.js";function m(e){const r=new Set;for(const t of e.recipes)r.add(t.entity);return[...r]}function A(e,r,t){if(!t)return[];const d=new Set;for(const i of r)E(i.text,e).mentions.forEach((c,f)=>{t(c,f)&&d.add(c.entity)});return m(e).filter(i=>d.has(i))}const l=e=>e.replace(/[&<>"]/g,r=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"})[r]),N=e=>e instanceof Error?e.message:String(e);function z(e){let r=0;for(let t=0;t<e.length;t++)r=r*31+e.charCodeAt(t)|0;return Math.abs(r)}function C(e){return`<table><thead><tr><th>span</th><th>leaf</th><th>mention</th><th>text</th></tr></thead><tbody>${e.leaves.map(t=>{const d=t.mention===void 0?"":`${t.mention} ${e.mentions[t.mention].entity}`;return`<tr><td>${t.start}\u2013${t.end}</td><td>${t.kind}${t.part?`:${t.part}`:""}</td><td>${d}</td><td><code>${l(JSON.stringify(e.text.slice(t.start,t.end)))}</code></td></tr>`}).join("")}</tbody></table>`}function y(e,r,t,d){try{const i=performance.now(),s=E(r,t),c=(performance.now()-i).toFixed(2);e.innerHTML=`
2
- <p class="fewrd-tagged">${H(s,d)}</p>
3
- <p class="fewrd-gist"><span class="fewrd-k">gist</span> ${l(J(s,d))}</p>
4
- <details><summary>${s.mentions.length} mentions \xB7 ${s.leaves.length} leaves \xB7 ${c} ms</summary>
5
- <pre>${l(JSON.stringify(s.mentions,null,1))}</pre>
6
- ${C(s)}
7
- </details>`}catch(i){e.innerHTML=`<p class="fewrd-error">${l(N(i))}</p>`}}function F(e,r){const{cases:t}=r,d="data"in r?r.resolvers:void 0,i="data"in r?JSON.stringify(r.data,null,2):"";let s,c=[];"book"in r?s=r.book:{book:s,errors:c}=O(r.data,{resolvers:d});const f=new Set(A(s,t,r.fold)),h=n=>f.has(n.entity),a=`fewrd-pg-${Math.random().toString(36).slice(2)}`;e.classList.add(a,"condensed");const b=document.createElement("style");document.head.appendChild(b);function v(){b.textContent=`
1
+ import{read as A,compile as C,html as H,gist as F}from"./index.js";function h(e){const t=new Set;for(const r of e.recipes)t.add(r.entity);return[...t]}function J(e,t,r){if(!r)return[];const i=new Set;for(const c of t)A(c.text,e).mentions.forEach((o,u)=>{r(o,u)&&i.add(o.entity)});return h(e).filter(c=>i.has(c))}const l=e=>e.replace(/[&<>"]/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"})[t]),b=e=>e instanceof Error?e.message:String(e);function B(e){let t=0;for(let r=0;r<e.length;r++)t=t*31+e.charCodeAt(r)|0;return Math.abs(t)}function D(e){return`<table><thead><tr><th>span</th><th>leaf</th><th>mention</th><th>text</th></tr></thead><tbody>${e.leaves.map(r=>{const i=r.mention===void 0?"":`${r.mention} ${e.mentions[r.mention].entity}`;return`<tr><td>${r.start}\u2013${r.end}</td><td>${r.kind}${r.part?`:${r.part}`:""}</td><td>${i}</td><td><code>${l(JSON.stringify(e.text.slice(r.start,r.end)))}</code></td></tr>`}).join("")}</tbody></table>`}function x(e,t,r,i){try{const c=performance.now(),d=A(t,r),o=(performance.now()-c).toFixed(2);e.innerHTML=`
2
+ <p class="fewrd-tagged">${H(d,i)}</p>
3
+ <p class="fewrd-gist"><span class="fewrd-k">gist</span> ${l(F(d,i))}</p>
4
+ <details><summary>${d.mentions.length} mentions \xB7 ${d.leaves.length} leaves \xB7 ${o} ms</summary>
5
+ <pre>${l(JSON.stringify(d.mentions,null,1))}</pre>
6
+ ${D(d)}
7
+ </details>`}catch(c){e.innerHTML=`<p class="fewrd-error">${l(b(c))}</p>`}}function I(e,t){const{cases:r}=t,i="data"in t?t.resolvers:void 0,c="data"in t?t.save:void 0;let d="data"in t?JSON.stringify(t.data,null,2):"",o,u=[];"book"in t?o=t.book:{book:o,errors:u}=C(t.data,{resolvers:i});const p=new Set(J(o,r,t.fold)),w=n=>p.has(n.entity),a=`fewrd-pg-${Math.random().toString(36).slice(2)}`;e.classList.add(a,"condensed");const S=document.createElement("style");document.head.appendChild(S);function k(){S.textContent=`
8
8
  .${a}.condensed [data-fold] { display: none; }
9
9
  .${a}:not(.condensed) [data-fold] { text-decoration: line-through; text-decoration-color: rgba(0,0,0,.35); }
10
10
  .${a} [data-entity] { background: var(--h); border-radius: 3px; padding: 0 1px; }
@@ -20,9 +20,10 @@ import{read as E,compile as O,html as H,gist as J}from"./index.js";function m(e)
20
20
  .${a} .fewrd-errors { margin: 6px 0; padding-left: 18px; font-size: 12px; }
21
21
  .${a} .fewrd-errors:empty { display: none; }
22
22
  .${a} .fewrd-errors, .${a} .fewrd-error { color: #b3261e; }
23
- ${m(s).map(n=>`.${a} [data-entity=${JSON.stringify(n)}] { --h: hsl(${z(n)%360}, 70%, 88%); }`).join(`
23
+ .${a} .fewrd-status { font-size: 12px; opacity: .7; }
24
+ ${h(o).map(n=>`.${a} [data-entity=${JSON.stringify(n)}] { --h: hsl(${B(n)%360}, 70%, 88%); }`).join(`
24
25
  `)}
25
- `}const T="data"in r?`<details class="fewrd-editor" open><summary>recipes <button type="button" class="fewrd-reset">reset</button></summary>
26
+ `}const T="data"in t?`<details class="fewrd-editor" open><summary>recipes <button type="button" class="fewrd-reset">reset</button>${c?' <button type="button" class="fewrd-save">save</button> <span class="fewrd-status"></span>':""}</summary>
26
27
  <textarea class="fewrd-book" spellcheck="false"></textarea>
27
28
  <ul class="fewrd-errors"></ul>
28
29
  </details>`:"";e.innerHTML=`
@@ -35,4 +36,4 @@ import{read as E,compile as O,html as H,gist as J}from"./index.js";function m(e)
35
36
  <textarea class="fewrd-custom" rows="3" spellcheck="false"></textarea>
36
37
  <div class="fewrd-trial-out"></div>
37
38
  </section>
38
- <div class="fewrd-cases"></div>`;const x=e.querySelector(".fewrd-condensed"),p=e.querySelector(".fewrd-folds"),w=e.querySelector(".fewrd-custom"),S=e.querySelector(".fewrd-trial-out"),k=e.querySelector(".fewrd-cases");k.innerHTML=t.map((n,o)=>`<section class="fewrd-case"><h2>${l(t[o].name)}</h2><div class="fewrd-out" data-i="${o}"></div></section>`).join("");const j=Array.from(k.querySelectorAll(".fewrd-out"));function L(){const n=m(s);for(const o of Array.from(f))n.includes(o)||f.delete(o);p.size=Math.max(n.length,1),p.innerHTML=n.map(o=>`<option value="${l(o)}"${f.has(o)?" selected":""}>${l(o)}</option>`).join("")}function $(){j.forEach(n=>y(n,t[Number(n.getAttribute("data-i"))].text,s,h)),y(S,w.value,s,h)}p.addEventListener("change",()=>{f.clear();for(const n of Array.from(p.selectedOptions))f.add(n.value);$()}),x.addEventListener("change",()=>e.classList.toggle("condensed",x.checked)),w.addEventListener("input",()=>y(S,w.value,s,h));const u=e.querySelector(".fewrd-book"),M=e.querySelector(".fewrd-errors");function g(n){M&&(M.innerHTML=n.map(o=>`<li>${l([o.path,o.entity&&`(${o.entity})`].filter(Boolean).join(" "))}${o.path?": ":""}${l(o.message)}</li>`).join(""))}function q(){if(!u)return;let n;try{n=JSON.parse(u.value)}catch(o){return g([{path:"",message:`invalid JSON, still showing the last good book: ${N(o)}`}])}({book:s,errors:c}=O(n,{resolvers:d})),g(c),v(),L(),$()}u&&(u.value=i,u.addEventListener("input",q),e.querySelector(".fewrd-reset").addEventListener("click",n=>{n.preventDefault(),u.value=i,q()})),g(c),v(),L(),$()}export{m as entities,A as initialSelection,F as mount};
39
+ <div class="fewrd-cases"></div>`;const L=e.querySelector(".fewrd-condensed"),m=e.querySelector(".fewrd-folds"),$=e.querySelector(".fewrd-custom"),q=e.querySelector(".fewrd-trial-out"),E=e.querySelector(".fewrd-cases");E.innerHTML=r.map((n,s)=>`<section class="fewrd-case"><h2>${l(r[s].name)}</h2><div class="fewrd-out" data-i="${s}"></div></section>`).join("");const j=Array.from(E.querySelectorAll(".fewrd-out"));function M(){const n=h(o);for(const s of Array.from(p))n.includes(s)||p.delete(s);m.size=Math.max(n.length,1),m.innerHTML=n.map(s=>`<option value="${l(s)}"${p.has(s)?" selected":""}>${l(s)}</option>`).join("")}function y(){j.forEach(n=>x(n,r[Number(n.getAttribute("data-i"))].text,o,w)),x(q,$.value,o,w)}m.addEventListener("change",()=>{p.clear();for(const n of Array.from(m.selectedOptions))p.add(n.value);y()}),L.addEventListener("change",()=>e.classList.toggle("condensed",L.checked)),$.addEventListener("input",()=>x(q,$.value,o,w));const f=e.querySelector(".fewrd-book"),O=e.querySelector(".fewrd-errors");function g(n){O&&(O.innerHTML=n.map(s=>`<li>${l([s.path,s.entity&&`(${s.entity})`].filter(Boolean).join(" "))}${s.path?": ":""}${l(s.message)}</li>`).join(""))}function N(){if(!f)return;let n;try{n=JSON.parse(f.value)}catch(s){return g([{path:"",message:`invalid JSON, still showing the last good book: ${b(s)}`}])}({book:o,errors:u}=C(n,{resolvers:i})),g(u),k(),M(),y()}if(f){f.value=d,f.addEventListener("input",N),e.querySelector(".fewrd-reset").addEventListener("click",s=>{s.preventDefault(),f.value=d,N()});const n=e.querySelector(".fewrd-status");e.querySelector(".fewrd-save")?.addEventListener("click",async s=>{s.preventDefault();const v=f.value;try{JSON.parse(v),await c(v),d=v,n.textContent="saved"}catch(z){n.textContent=`not saved: ${b(z)}`}}),f.addEventListener("input",()=>n&&(n.textContent=""))}g(u),k(),M(),y()}export{h as entities,J as initialSelection,I as mount};
@@ -16,6 +16,8 @@ export type MountOptions = (CommonOptions & {
16
16
  | (CommonOptions & {
17
17
  data: unknown;
18
18
  resolvers?: Readonly<Record<string, Resolver>>;
19
+ /** Shows a save button: called with the editor's text once it parses as JSON. Reset then returns to what was saved. */
20
+ save?: (json: string) => unknown;
19
21
  });
20
22
  /** Distinct entities across a book's recipes, in first-appearance order. */
21
23
  export declare function entities(book: Book): string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fewrd",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Finds what recurs in a string, cuts it loose, and gets you the gist in a few words.",