@symbo.ls/brender 3.14.678 → 3.14.679
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/CHANGELOG.md +22 -0
- package/README.md +2 -0
- package/dist/esm/drawnFaces.js +3 -0
- package/dist/esm/index.js +1 -1
- package/drawnFaces.js +783 -0
- package/index.js +7 -2
- package/package.json +11 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @symbo.ls/brender
|
|
2
2
|
|
|
3
|
+
## 3.14.679
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- c80283f: The faces a built page takes are the faces it draws (XMA r12 results, item 2). `window.__smblsFacesReady` and `brender.faces.inline: true` took every face of a family the page's styles name when any character of the page was in its range: a page that draws a family only in bold headings asked for its regular face too, and a page that draws TBCContractica 300/400/500/700 loaded 100 and 900 as well.
|
|
8
|
+
|
|
9
|
+
`@symbo.ls/brender` exports `drawnFaces(html, { readSheet })` (also `@symbo.ls/brender/drawnFaces`): which `@font-face` rules of the page's faces sheet its elements draw, read as the browser picks them — each element's family, weight and style from the cascade (`!important`, specificity, order, inheritance, `var()`, the `font` shorthand, the browser's bold headings, italic emphasis and system-font form controls), each character of its own text in the face of its family list that holds it (style, then weight, then `unicode-range`). Rules under `@media` / `@supports` / `@container` / `@layer` and for states (`:hover`, `:focus`, …) count as possibilities where they would win; `::placeholder` and the strings of `::before` / `::after` count. It returns `Map<rule index, characters>`, or `null` when a stylesheet of the page cannot be read.
|
|
10
|
+
|
|
11
|
+
`smbls build` uses it: `inline: true` inlines the faces the page draws, the list form the drawn faces of the listed families, and the function form gets `face.drawn` (the characters drawn in that face; `null` when the page was not read). The faces promise loads the drawn faces by address, each with a character drawn in it. The page's `<link rel="stylesheet">` files and their `@import`s are read from the build output; a page with a stylesheet the build cannot read, or a build with a brender that predates the reader, keeps the earlier rule. The log says on how many pages the drawn faces were read (`Faces drawn: read on N of M pages`), and the report says it per page (`facesRead`).
|
|
12
|
+
|
|
13
|
+
- Updated dependencies [bfef7bf]
|
|
14
|
+
- Updated dependencies [0e57128]
|
|
15
|
+
- Updated dependencies [9f253fc]
|
|
16
|
+
- Updated dependencies [9f253fc]
|
|
17
|
+
- Updated dependencies [de2f734]
|
|
18
|
+
- Updated dependencies [d9194ce]
|
|
19
|
+
- @symbo.ls/element@3.14.819
|
|
20
|
+
- @symbo.ls/utils@3.14.855
|
|
21
|
+
- smbls@3.14.862
|
|
22
|
+
- @symbo.ls/fetch@3.14.675
|
|
23
|
+
- @symbo.ls/helmet@3.14.674
|
|
24
|
+
|
|
3
25
|
## 3.14.678
|
|
4
26
|
|
|
5
27
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -209,6 +209,8 @@ No inline script — a strict Content-Security-Policy (`script-src 'self'`, no `
|
|
|
209
209
|
|
|
210
210
|
The marks describe the faces by address. `smbls build` marks each page again after its `file:` URLs point into the build output, so the version follows the files the page ships (every page of a build carries the same one when the font URLs are site-root or absolute; a relative URL differs by the page's directory, and so does the version). Its `brender.faces.inline` step moves files into the sheet as `data:` URLs and keeps both marks as they were: an inlined face still names its file. A tool that changes the addresses in a page itself calls `markFacesSheet(html)` (exported): it writes both marks again from the rules the sheet holds, leaves the sheet's text and its other attributes as they are, and returns a page without a faces sheet unchanged. `facesSheetMarks(rules)` returns `{ version, files }` for a sheet's text or its rules (`null` without a `@font-face`).
|
|
211
211
|
|
|
212
|
+
**The faces a page draws.** `drawnFaces(html, { readSheet })` (exported, also as `@symbo.ls/brender/drawnFaces`) reads which faces of the sheet the page's elements draw, as the browser picks them: each element's family, weight and style from the cascade (`!important`, specificity, order, inheritance, `var()`, the `font` shorthand, the browser's own rules for headings, emphasis and form controls), each character of its text against the faces' `unicode-range`. Rules under `@media` / `@supports` / `@container` / `@layer` and for states (`:hover`, …) count as possibilities. It returns a `Map` from a `@font-face` rule's index in the sheet to the characters drawn in it, or `null` when a stylesheet of the page cannot be read: `readSheet(href, from)` returns the text of a `<link rel="stylesheet">` file or an `@import` (`from`: the address of the sheet that imports it), or `null`. A `<link data-smbls-font>` (a font service's `@font-face` sheet) is not read. `smbls build` uses it for `brender.faces.inline: true` and `window.__smblsFacesReady` (CLI README, "Which faces a page draws").
|
|
213
|
+
|
|
212
214
|
The runtime also reads what pages from brender up to 3.14.664 carry — the inline `window.__BRENDER__` flag, `window.__BR_REGISTRY__`, `window.__BRENDER_HYDRATE__` — and, when a policy blocked that flag, takes a `data-br` element directly in the mount node for the mark. An smbls runtime older than this format does not know the `<meta>`: keep `smbls` and `@symbo.ls/brender` at the same release.
|
|
213
215
|
|
|
214
216
|
**The data is the build's.** A pre-rendered page shows the answers its render got until it is rendered again — a `smbls build` page until the next build. A page flagged stale (`<html data-smbls-stale>` from the host, or the project's `fetch.seed.isStale(stamp)` / `fetch.seed.maxAge`) shows them at once and then, after the adoption, asks again and swaps in what changed; `fetch.seed: false` keeps the answers out of the page. See `@symbo.ls/fetch`, "A pre-built page shows the data of its build".
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{parseHTML as ue}from"linkedom";import{stylesheetRules as he}from"./pageWeight.js";const G=/<style\b[^>]*\bdata-smbls-faces\b[^>]*>([\s\S]*?)<\/style\s*>/i,me=/@font-face\s*\{([^}]*)\}/gi,de=/<(script|template|noscript)\b([^>]*)>[\s\S]*?<\/\1\s*>/gi,H=8,J=new Set(["serif","sans-serif","monospace","cursive","fantasy","system-ui","ui-serif","ui-sans-serif","ui-monospace","ui-rounded","math","emoji","fangsong","-apple-system","blinkmacsystemfont"]),pe=new Set(["hover","focus","focus-visible","focus-within","active","visited","link","any-link","target","target-within","checked","indeterminate","default","disabled","enabled","read-only","read-write","required","optional","valid","invalid","user-valid","user-invalid","in-range","out-of-range","placeholder-shown","autofill","-webkit-autofill","open","closed","popover-open","modal","fullscreen","picture-in-picture","playing","paused","current","past","future","defined","lang","dir","state","blank","local-link","scope","host","host-context"]),V=new Set(["before","after","first-line","first-letter"]),B=new Set(["is","where","matches","-webkit-any","-moz-any","has"]),ge=`h1,h2,h3,h4,h5,h6,th{font-weight:bold}b,strong{font-weight:bolder}
|
|
2
|
+
em,i,cite,var,dfn,address{font-style:italic}code,kbd,samp,pre,tt,listing,xmp,plaintext{font-family:monospace}
|
|
3
|
+
input,textarea,select,button{font-family:system-ui;font-weight:400;font-style:normal}`,P=new Set(["font","font-family","font-weight","font-style"]),F=/^\s*(?:only\s+)?(?:all|screen)\s*$/i,K=/var\(\s*(--[\w-]+)/g,q=(t,s)=>{let n=0,o=null;for(let r=s;r<t.length;r++){const e=t[r];if(o){e==="\\"?r++:e===o&&(o=null);continue}if(e==='"'||e==="'")o=e;else if(e==="(")n++;else if(e===")"&&!--n)return r}return-1},L=(t,s)=>{const n=[];let o=0,r=null,e=0;for(let i=0;i<t.length;i++){const l=t[i];if(r){l==="\\"?i++:l===r&&(r=null);continue}l==='"'||l==="'"?r=l:l==="("||l==="["||l==="{"?o++:l===")"||l==="]"||l==="}"?o=Math.max(0,o-1):l===s&&!o&&(n.push(t.slice(e,i)),e=i+1)}return n.push(t.slice(e)),n},X=/[\w\- -]/,C=(t,s)=>{let n=s;for(;n<t.length;)if(t[n]==="\\")n+=2;else if(X.test(t[n]))n++;else break;return n},Q=t=>{let s=0,n=0,o=0;const r=e=>{s+=Math.floor(e/1e6),n+=Math.floor(e/1e3)%1e3,o+=e%1e3};for(let e=0;e<t.length;){const i=t[e];if(i==='"'||i==="'"){const l=t.indexOf(i,e+1);e=l===-1?t.length:l+1}else if(i==="#")s++,e=C(t,e+1);else if(i===".")n++,e=C(t,e+1);else if(i==="["){n++;const l=t.indexOf("]",e);e=l===-1?t.length:l+1}else if(i===":"){const l=t[e+1]===":",u=e+(l?2:1),f=C(t,u),h=t.slice(u,f).toLowerCase();let g=null,a=f;if(t[f]==="("){const m=q(t,f);g=t.slice(f+1,m===-1?t.length:m),a=m===-1?t.length:m+1}l||V.has(h)?o++:h==="where"||(B.has(h)||h==="not"?g&&r(Math.max(0,...L(g,",").map(m=>Q(m.trim())))):n++),e=a}else i==="*"||i===">"||i==="+"||i==="~"||i==="|"||/\s/.test(i)?e++:X.test(i)||i==="\\"?(o++,e=C(t,e)):e++}return s*1e6+n*1e3+o},Z=t=>{let s=!1,n="element";return{text:(e=>{let i="";for(let l=0;l<e.length;){const u=e[l];if(u==='"'||u==="'"){const f=e.indexOf(u,l+1),h=f===-1?e.length:f+1;i+=e.slice(l,h),l=h}else if(u==="["){const f=e.indexOf("]",l),h=f===-1?e.length:f+1;i+=e.slice(l,h),l=h}else if(u===":"){const f=e[l+1]===":",h=l+(f?2:1),g=C(e,h),a=e.slice(h,g).toLowerCase();let m=null,O=g;if(e[g]==="("){const d=q(e,g);m=e.slice(g+1,d===-1?e.length:d),O=d===-1?e.length:d+1}if(f||V.has(a))a==="placeholder"||a==="-webkit-input-placeholder"||a==="-moz-placeholder"?n="placeholder":a==="before"||a==="after"?n=a:a==="first-line"||a==="first-letter"?s=!0:n=null,i+=":is(*)";else if(a==="not"&&m!==null){const d=ee(m);d.conditional?(s=!0,i+=":is(*)"):i+=`:not(${d.text})`}else if(B.has(a)&&m!==null){const d=ee(m);d.conditional&&(s=!0),i+=`:${a==="matches"||a.endsWith("-any")?"is":a}(${d.text||"*"})`}else pe.has(a)||a.startsWith("-webkit-")||a.startsWith("-moz-")?(s=!0,i+=":is(*)"):i+=e.slice(l,O);l=O}else i+=u,l++}return i})(t),conditional:s,target:n}},ye=t=>{let s="";for(let n=0;n<t.length;){const o=t[n];if(o==='"'||o==="'"||o==="["){const r=t.indexOf(o==="["?"]":o,n+1),e=r===-1?t.length:r+1;s+=t.slice(n,e),n=e}else if(o===":"){let r=C(t,n+(t[n+1]===":"?2:1));if(t[r]==="("){const e=q(t,r);r=e===-1?t.length:e+1}s+=":is(*)",n=r}else s+=o,n++}return s},ee=t=>{let s=!1;return{text:L(t,",").map(o=>{const r=Z(o.trim());return r.conditional&&(s=!0),r.text||"*"}).join(", "),conditional:s}},te=t=>{const s=[];for(const n of L(t.replace(/\/\*[\s\S]*?\*\//g,""),";")){if(n.includes("{"))continue;const o=n.indexOf(":");if(o===-1)continue;const r=n.slice(0,o).trim(),e=r.startsWith("--")?r:r.toLowerCase();if(!P.has(e)&&e!=="content"&&!e.startsWith("--"))continue;let i=n.slice(o+1).trim();const l=/!\s*important\s*$/i.test(i);l&&(i=i.replace(/!\s*important\s*$/i,"").trim()),s.push({prop:e,value:i,important:l})}return s},we=/^@import\s+(?:url\(\s*)?(['"]?)([^'")\s]+)\1\s*\)?\s*([^;]*)/i,T=(t,s,n,o,r=null,e=[])=>{const i=he(t);if(!i)return n.unknown=!0,e;for(const l of i){const u=l.indexOf("{");if(u===-1){const a=we.exec(l);if(!a)continue;const m=n.readSheet?n.readSheet(a[2],o):null;typeof m!="string"?n.unknown=!0:T(m,s||!!a[3].trim()&&!F.test(a[3]),n,a[2],r,e);continue}if(!l.endsWith("}"))continue;const f=l.slice(0,u).trim(),h=l.slice(u+1,-1);if(f[0]==="@"){const a=(/^@([\w-]+)/.exec(f)||[])[1]?.toLowerCase();a==="media"?T(h,s||!F.test(f.slice(6)),n,o,r,e):(a==="supports"||a==="container"||a==="document"||a==="scope"||a==="starting-style"||a==="layer")&&T(h,!0,n,o,r,e);continue}const g=r?L(f,",").map(a=>a.includes("&")?a.replace(/&/g,`:is(${r})`):`:is(${r}) ${a.trim()}`).join(", "):f;if(e.push({selector:g,block:h,conditional:s}),h.includes("{")){const a=h.slice(h.search(/[^;{}]*\{/));T(a,s,n,o,g,e)}}return e},N=(t,s,n=0)=>{const o=t.indexOf("var(");if(o===-1)return[t];if(n>8)return[];const r=q(t,o+3);if(r===-1)return[];const e=t.slice(o+4,r),i=L(e,","),l=i[0].trim(),u=i.length>1?i.slice(1).join(",").trim():null,f=s.get(l),h=f&&f.length?f:u!==null?N(u,s,n+1):[],g=[];for(const a of h)for(const m of N(t.slice(0,o)+a+t.slice(r+1),s,n+1))if(g.includes(m)||g.push(m),g.length>=H)return g;return g},ne=t=>t.trim().replace(/^(['"])([\s\S]*)\1$/,"$2").replace(/\\(.)/g,"$1").trim().toLowerCase(),be=t=>L(t,",").map(ne).filter(Boolean),se={normal:400,bold:700},oe=(t,s)=>{const n=String(t).trim().toLowerCase();if(n in se)return se[n];if(n==="bolder")return s<350?400:s<550?700:900;if(n==="lighter")return s<550?100:s<750?400:700;const o=Number(n);return Number.isFinite(o)&&o>=1&&o<=1e3?o:null},ie=t=>{const s=String(t).trim().toLowerCase().split(/\s+/)[0];return s==="italic"||s==="oblique"||s==="normal"?s:null},Se=/\\([0-9a-f]{1,6})\s?|\\([\s\S])/gi,ke=(t,s)=>{let n="";for(const o of t.matchAll(/(["'])((?:\\[\s\S]|(?!\1)[^\\])*)\1|attr\(\s*([\w-]+)[^)]*\)/gi))o[3]?n+=s.getAttribute(o[3])||"":n+=o[2].replace(Se,(r,e,i)=>{if(!e)return i;const l=parseInt(e,16);return l>0&&l<=1114111?String.fromCodePoint(l):"\uFFFD"});return n},ve=new Set(["caption","icon","menu","message-box","small-caption","status-bar"]),Oe=t=>{const s=t.trim();if(ve.has(s.toLowerCase()))return{family:"system-ui",weight:"normal",style:"normal"};const n=[];let o=s;for(;;){const u=/^\s*([^\s,'"]+)/.exec(o);if(!u)break;const f=u[1];if(/^[\d.]/.test(f)&&!/^\d{1,4}$/.test(f)||/^\d{1,4}$/.test(f)&&Number(f)>1e3)break;const h=f.toLowerCase();if(/^(xx-small|x-small|small|medium|large|x-large|xx-large|xxx-large|larger|smaller|calc\(|clamp\(|min\(|max\()/.test(h)||(n.push(h),o=o.slice(u[0].length),n.length>4))break}const r=/^\s*(?:[^\s/]+|(?:calc|clamp|min|max)\([^)]*\))(?:\s*\/\s*[^\s]+)?\s+/.exec(o);if(!r)return null;const e=o.slice(r[0].length).trim();if(!e)return null;let i="normal",l="normal";for(const u of n)u==="italic"||u==="oblique"?l=u:(u==="bold"||u==="bolder"||u==="lighter"||/^\d{1,4}$/.test(u))&&(i=u);return{family:e,weight:i,style:l}},_=(t,s)=>{const n=new RegExp(`(?:^|;|\\s)${s}\\s*:\\s*([^;]+)`,"i").exec(t);return n?n[1].trim():null},Ae=t=>{if(!t)return null;const s=[];for(const n of String(t).split(",")){const o=n.trim().replace(/^u\+/i,"");if(!o)continue;if(o.includes("?")){s.push([parseInt(o.replace(/\?/g,"0"),16),parseInt(o.replace(/\?/g,"f"),16)]);continue}const[r,e]=o.split("-"),i=parseInt(r,16);s.push([i,e?parseInt(e,16):i])}return s.length?s:null},Ee=t=>{const s=[];let n=0;for(const o of t.matchAll(me)){const r=o[1],e=ne(_(r,"font-family")||""),i=String(_(r,"font-weight")||"400").trim().split(/\s+/).map(l=>oe(l,400)||400);s.push({index:n++,family:e,min:Math.min(...i),max:Math.max(...i),style:ie(_(r,"font-style")||"normal")||"normal",ranges:Ae(_(r,"unicode-range"))})}return s},le={italic:["italic","oblique","normal"],oblique:["oblique","italic","normal"],normal:["normal","oblique","italic"]},xe=(t,s)=>{for(const n of le[s]||le.normal){const o=t.filter(r=>r.style===n);if(o.length)return o}return t},Me=(t,s)=>{const n=t.filter(i=>i.min<=s&&s<=i.max);if(n.length)return n;const o=t.filter(i=>i.max<s).sort((i,l)=>l.max-i.max),r=t.filter(i=>i.min>s).sort((i,l)=>i.min-l.min);let e;return s>=400&&s<=500?e=r.find(i=>i.min<=500)||o[0]||r[0]:s<400?e=o[0]||r[0]:e=r[0]||o[0],e?t.filter(i=>i.min===e.min&&i.max===e.max):[]},Le=(t,s)=>!t.ranges||t.ranges.some(([n,o])=>s>=n&&s<=o),re=t=>t.ua?0:t.important?2:1,ce=(t,s)=>{const n=re(t),o=re(s);return n!==o?n>o:t.rank!==s.rank?t.rank>s.rank:t.order>s.order},D=t=>{let s=null;for(const o of t)!o.conditional&&(!s||ce(o,s))&&(s=o);const n=s?[s]:[];for(const o of t)o.conditional&&(!s||ce(o,s))&&n.push(o);return{list:n,inherit:!s}},U=t=>{const s=[],n=new Set;for(const o of t){const r=JSON.stringify(o);if(!n.has(r)&&(n.add(r),s.push(o),s.length>=H))break}return s},ae=(t,s)=>{K.lastIndex=0;let n;for(;n=K.exec(t);)s.add(n[1]);return s},fe={vars:new Map,family:[["serif"]],weight:[400],style:["normal"]},Ce=new Set(["script","style","template","noscript","head","title"]),Y=(t,s)=>{const n=new Map(s.vars),o=[...new Set(t.filter(e=>e.prop.startsWith("--")).map(e=>e.prop))];for(const e of o){const{list:i,inherit:l}=D(t.filter(f=>f.prop===e)),u=l?[...s.vars.get(e)||[]]:[];for(const f of i){const h=f.value.trim();/^(inherit|unset)$/i.test(h)?u.push(...s.vars.get(e)||[]):/^(initial|revert|revert-layer)$/i.test(h)||u.push(h)}n.set(e,U(u))}for(const e of o)n.set(e,U(n.get(e).flatMap(i=>N(i,n))));const r={vars:n};for(const e of["family","weight","style"]){const i="font-"+e,{list:l,inherit:u}=D(t.filter(h=>h.prop===i||h.prop==="font")),f=u?[...s[e]]:[];for(const h of l){const g=f.length;for(const a of N(h.value,n)){const m=a.trim().toLowerCase();if(m==="inherit"||m==="unset"){f.push(...s[e]);continue}if(m==="initial"||m==="revert"||m==="revert-layer"){f.push(...fe[e]);continue}let O=a;if(h.prop==="font"){const d=Oe(a);if(!d)continue;O=d[e]}if(e==="family")f.push(be(O));else if(e==="style"){const d=ie(O);d&&f.push(d)}else for(const d of s.weight){const $=oe(O,d);$&&f.push($)}}f.length===g&&f.push(...s[e])}r[e]=U(f)}return r},Ne=(t,s={})=>{const n=new Map,o=G.exec(t);if(!o)return n;const r=Ee(o[1]);if(!r.length)return n;const e=new Map;for(const c of r)e.has(c.family)||e.set(c.family,[]),e.get(c.family).push(c);const i=t.replace(G,"<style data-smbls-faces></style>").replace(de,"<$1$2></$1>"),{document:l}=ue(i),u={readSheet:typeof s.readSheet=="function"?s.readSheet:null,unknown:!1},f=[],h=(c,y,w,b)=>{for(const S of T(c,y,u,b)){const p=te(S.block);p.length&&f.push({selector:S.selector,decls:p,conditional:S.conditional,ua:w})}};h(ge,!1,!0);for(const c of l.querySelectorAll("style, link")){const y=c.getAttribute("media"),w=!!y&&!F.test(y);if(c.localName==="style"){c.hasAttribute("data-smbls-faces")||h(c.textContent||"",w,!1);continue}const b=(c.getAttribute("rel")||"").toLowerCase().split(/\s+/);if(!b.includes("stylesheet")||c.hasAttribute("data-smbls-font")||c.hasAttribute("disabled"))continue;const S=c.getAttribute("href")||"",p=u.readSheet&&S?u.readSheet(S):null;if(typeof p!="string")return null;h(p,w||b.includes("alternate"),!1,S)}if(u.unknown)return null;const g=[];for(const c of l.querySelectorAll("[style]")){const y=te(c.getAttribute("style")||"");y.length&&g.push({el:c,decls:y})}const a=new Set,m=[...f.flatMap(c=>c.decls),...g.flatMap(c=>c.decls)];for(const c of m)(P.has(c.prop)||c.prop==="content")&&ae(c.value,a);for(let c=-1;c!==a.size;){c=a.size;for(const y of m)a.has(y.prop)&&ae(y.value,a)}const O=c=>P.has(c.prop)||c.prop==="content"||a.has(c.prop),d={element:new Map,placeholder:new Map,before:new Map,after:new Map};let $=0;for(const c of f){const y=c.decls.filter(O);if(y.length)for(const w of L(c.selector,",")){const b=w.trim();if(!b)continue;let{text:S,conditional:p,target:E}=Z(b);if(!E)continue;let k;try{k=l.querySelectorAll(S)}catch{try{k=l.querySelectorAll(ye(S))}catch{continue}p=!0}const A=c.ua?0:Q(b),x=y.map(v=>({...v,rank:A,order:++$,conditional:p||c.conditional,ua:c.ua})),M=d[E];for(const v of k)M.has(v)||M.set(v,[]),M.get(v).push(...x)}}for(const{el:c,decls:y}of g){const w=y.filter(O);w.length&&(d.element.has(c)||d.element.set(c,[]),d.element.get(c).push(...w.map(b=>({...b,rank:1e9,order:++$,conditional:!1,ua:!1}))))}const I=new Map,W=(c,y)=>{if(y.trim())for(const w of c.family)for(const b of c.weight)for(const S of c.style){const p=JSON.stringify([w,b,S]);I.set(p,(I.get(p)||"")+y)}},j=(c,y)=>{const w=Y(d.element.get(c)||[],y);let b="";for(const p of c.childNodes)p.nodeType===3?b+=p.textContent:p.nodeType===1&&!Ce.has(p.localName)&&j(p,w);const S=c.localName;if(S==="input"||S==="textarea"){b+=" "+(c.getAttribute("value")||"");const p=c.getAttribute("placeholder");p&&W(d.placeholder.has(c)?Y(d.placeholder.get(c),w):w,p)}W(w,b);for(const p of["before","after"]){const E=d[p].get(c);if(!E)continue;const k=Y(E,w);for(const A of D(E.filter(x=>x.prop==="content")).list)for(const x of N(A.value,k.vars))W(k,ke(x,c))}};l.documentElement&&j(l.documentElement,fe);for(const[c,y]of I){const[w,b,S]=JSON.parse(c),p=new Map;for(const k of w){if(J.has(k))break;const A=e.get(k);A&&!p.has(k)&&p.set(k,Me(xe(A,S),b))}if(!p.size)continue;const E=new Set;for(const k of y){if(E.has(k))continue;E.add(k);const A=k.codePointAt(0);if(!(A<=32||A===160))for(const x of w){if(J.has(x))break;const M=p.get(x)||[];let v=null;for(let R=M.length-1;R>=0&&!v;R--)Le(M[R],A)&&(v=M[R]);if(!v)continue;const z=n.get(v.index)||"";z.includes(k)||n.set(v.index,z+k);break}}}return n};export{Ne as drawnFaces};
|
package/dist/esm/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createEnv as e}from"./env.js";import{resetKeys as r,assignKeys as t,mapKeysToElements as o}from"./keys.js";import{loadProject as a,loadAndRenderAll as m}from"./load.js";import{render as s,renderElement as p,renderRoute as i,renderPage as l,resetGlobalCSSCache as d,getAccumulatedEmotionCSS as n,replaceEmotionCSS as c,adoptionScripts as f,prerenderParts as S,buildStamp as g}from"./render.js";import{extractMetadata as h,generateHeadHtml as u}from"./metadata.js";import{collectBrNodes as P,hydrate as y}from"./hydrate.js";import{generateSitemap as C}from"./sitemap.js";import{prefetchPageData as E,injectPrefetchedState as R}from"./prefetch.js";import{composeIntoShell as b}from"./shell.js";import{trimSpriteToPage as k,classRulesForPage as x,markupClasses as A,trimVariablesToPage as
|
|
1
|
+
import{createEnv as e}from"./env.js";import{resetKeys as r,assignKeys as t,mapKeysToElements as o}from"./keys.js";import{loadProject as a,loadAndRenderAll as m}from"./load.js";import{render as s,renderElement as p,renderRoute as i,renderPage as l,resetGlobalCSSCache as d,getAccumulatedEmotionCSS as n,replaceEmotionCSS as c,adoptionScripts as f,prerenderParts as S,buildStamp as g}from"./render.js";import{extractMetadata as h,generateHeadHtml as u}from"./metadata.js";import{collectBrNodes as P,hydrate as y}from"./hydrate.js";import{generateSitemap as C}from"./sitemap.js";import{prefetchPageData as E,injectPrefetchedState as R}from"./prefetch.js";import{composeIntoShell as b}from"./shell.js";import{trimSpriteToPage as k,classRulesForPage as x,markupClasses as A,trimVariablesToPage as F,stylesheetRules as K}from"./pageWeight.js";import{markFacesSheet as T,facesSheetMarks as j}from"./faces.js";import{drawnFaces as H}from"./drawnFaces.js";var L={createEnv:e,resetKeys:r,assignKeys:t,mapKeysToElements:o,loadProject:a,loadAndRenderAll:m,render:s,renderElement:p,renderRoute:i,renderPage:l,resetGlobalCSSCache:d,getAccumulatedEmotionCSS:n,replaceEmotionCSS:c,extractMetadata:h,generateHeadHtml:u,collectBrNodes:P,hydrate:y,generateSitemap:C,prefetchPageData:E,injectPrefetchedState:R,composeIntoShell:b,adoptionScripts:f,prerenderParts:S,buildStamp:g,trimSpriteToPage:k,classRulesForPage:x,markupClasses:A,trimVariablesToPage:F,stylesheetRules:K,markFacesSheet:T,facesSheetMarks:j,drawnFaces:H};export{f as adoptionScripts,t as assignKeys,g as buildStamp,x as classRulesForPage,P as collectBrNodes,b as composeIntoShell,e as createEnv,L as default,H as drawnFaces,h as extractMetadata,j as facesSheetMarks,u as generateHeadHtml,C as generateSitemap,n as getAccumulatedEmotionCSS,y as hydrate,R as injectPrefetchedState,m as loadAndRenderAll,a as loadProject,o as mapKeysToElements,T as markFacesSheet,A as markupClasses,E as prefetchPageData,S as prerenderParts,s as render,p as renderElement,l as renderPage,i as renderRoute,c as replaceEmotionCSS,d as resetGlobalCSSCache,r as resetKeys,K as stylesheetRules,k as trimSpriteToPage,F as trimVariablesToPage};
|
package/drawnFaces.js
ADDED
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
// ── The faces a built page's elements draw ──────────────────────────────────
|
|
2
|
+
// XMA r12 results (mirc #503, item 2): window.__smblsFacesReady and
|
|
3
|
+
// `brender.faces.inline: true` took every face of a family the page's styles
|
|
4
|
+
// name, when any character of the whole page was in its range — a page that
|
|
5
|
+
// draws Playfair Display only in bold headings still asked for its regular
|
|
6
|
+
// face, ~60 KB a page never needs.
|
|
7
|
+
//
|
|
8
|
+
// This reads the page as the browser would draw it: each element's
|
|
9
|
+
// font-family, font-weight and font-style from the rules that match it (the
|
|
10
|
+
// cascade: origin, !important, specificity, order; inheritance; var(); the
|
|
11
|
+
// `font` shorthand; the browser's own bold headings, italic emphasis and
|
|
12
|
+
// system-font form controls), then the characters of the element's own text
|
|
13
|
+
// against the faces sheet (`<style data-smbls-faces>`) by the browser's face
|
|
14
|
+
// matching (style, then weight; then unicode-range per character, falling to
|
|
15
|
+
// the next family for a character no face of a family covers).
|
|
16
|
+
//
|
|
17
|
+
// What can change after the first view counts too, so no face that may be
|
|
18
|
+
// drawn is left out: a rule under @media / @supports / @container / @layer,
|
|
19
|
+
// and a rule whose selector names a state (:hover, :focus, :checked, …) or
|
|
20
|
+
// :lang(), adds its value as a possibility wherever it would win. A rule for
|
|
21
|
+
// `::placeholder` reaches the field's placeholder, one for `::before` /
|
|
22
|
+
// `::after` the strings and attr() of its `content`; other generated content
|
|
23
|
+
// (`::marker`, counters' own text) is not read. A name in a family list that
|
|
24
|
+
// the sheet does not declare (a system font) is passed over, so the next
|
|
25
|
+
// family counts for every character; a generic family (`sans-serif`,
|
|
26
|
+
// `system-ui`, …) ends the list.
|
|
27
|
+
//
|
|
28
|
+
// The page's stylesheets are its `<style>` elements, its
|
|
29
|
+
// `<link rel="stylesheet">` files and their @import files, read by
|
|
30
|
+
// `options.readSheet(href, from)` (`from`: the address of the sheet that
|
|
31
|
+
// imports it, none for the page) — a `<link data-smbls-font>` (a font
|
|
32
|
+
// service's sheet of @font-face rules, smbls fontFaces.js) is not read.
|
|
33
|
+
//
|
|
34
|
+
// Returns Map<index, chars>: `index` the n-th `@font-face` rule of the faces
|
|
35
|
+
// sheet (in order, as /@font-face\s*\{([^}]*)\}/g finds them), `chars` the
|
|
36
|
+
// characters the page draws in it (unique, in order of first use) — or null
|
|
37
|
+
// when a stylesheet of the page cannot be read (no readSheet, another origin,
|
|
38
|
+
// a sheet that does not parse): any face may be drawn then.
|
|
39
|
+
import { parseHTML } from 'linkedom'
|
|
40
|
+
import { stylesheetRules } from './pageWeight.js'
|
|
41
|
+
|
|
42
|
+
const FACES_STYLE = /<style\b[^>]*\bdata-smbls-faces\b[^>]*>([\s\S]*?)<\/style\s*>/i
|
|
43
|
+
const FACE_RULE = /@font-face\s*\{([^}]*)\}/gi
|
|
44
|
+
const RAW = /<(script|template|noscript)\b([^>]*)>[\s\S]*?<\/\1\s*>/gi
|
|
45
|
+
|
|
46
|
+
// At most this many values per property and element: a page whose
|
|
47
|
+
// variables branch more is read with the first ones.
|
|
48
|
+
const MAX_VALUES = 8
|
|
49
|
+
|
|
50
|
+
const GENERIC = new Set([
|
|
51
|
+
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui', 'ui-serif', 'ui-sans-serif',
|
|
52
|
+
'ui-monospace', 'ui-rounded', 'math', 'emoji', 'fangsong', '-apple-system', 'blinkmacsystemfont'
|
|
53
|
+
])
|
|
54
|
+
|
|
55
|
+
// Selector states: matched as if they held (the value is a possibility).
|
|
56
|
+
const STATES = new Set([
|
|
57
|
+
'hover', 'focus', 'focus-visible', 'focus-within', 'active', 'visited', 'link', 'any-link', 'target',
|
|
58
|
+
'target-within', 'checked', 'indeterminate', 'default', 'disabled', 'enabled', 'read-only', 'read-write',
|
|
59
|
+
'required', 'optional', 'valid', 'invalid', 'user-valid', 'user-invalid', 'in-range', 'out-of-range',
|
|
60
|
+
'placeholder-shown', 'autofill', '-webkit-autofill', 'open', 'closed', 'popover-open', 'modal',
|
|
61
|
+
'fullscreen', 'picture-in-picture', 'playing', 'paused', 'current', 'past', 'future', 'defined',
|
|
62
|
+
'lang', 'dir', 'state', 'blank', 'local-link', 'scope', 'host', 'host-context'
|
|
63
|
+
])
|
|
64
|
+
const LEGACY_PSEUDO_ELEMENTS = new Set(['before', 'after', 'first-line', 'first-letter'])
|
|
65
|
+
const SELECTOR_LISTS = new Set(['is', 'where', 'matches', '-webkit-any', '-moz-any', 'has'])
|
|
66
|
+
|
|
67
|
+
// The browser's own rules (lowest in the cascade): bold headings, italic
|
|
68
|
+
// emphasis, a system font in form controls (`font: inherit` undoes it).
|
|
69
|
+
const UA_RULES = `h1,h2,h3,h4,h5,h6,th{font-weight:bold}b,strong{font-weight:bolder}
|
|
70
|
+
em,i,cite,var,dfn,address{font-style:italic}code,kbd,samp,pre,tt,listing,xmp,plaintext{font-family:monospace}
|
|
71
|
+
input,textarea,select,button{font-family:system-ui;font-weight:400;font-style:normal}`
|
|
72
|
+
|
|
73
|
+
const FONT_PROPS = new Set(['font', 'font-family', 'font-weight', 'font-style'])
|
|
74
|
+
const ALWAYS_MEDIA = /^\s*(?:only\s+)?(?:all|screen)\s*$/i
|
|
75
|
+
const VAR_NAME = /var\(\s*(--[\w-]+)/g
|
|
76
|
+
|
|
77
|
+
// ── Text helpers ────────────────────────────────────────────────────────────
|
|
78
|
+
// The end of a balanced (…) that opens at `at` (quotes skipped); -1 if none.
|
|
79
|
+
const closeOf = (text, at) => {
|
|
80
|
+
let depth = 0
|
|
81
|
+
let quote = null
|
|
82
|
+
for (let i = at; i < text.length; i++) {
|
|
83
|
+
const ch = text[i]
|
|
84
|
+
if (quote) {
|
|
85
|
+
if (ch === '\\') i++
|
|
86
|
+
else if (ch === quote) quote = null
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
if (ch === '"' || ch === "'") quote = ch
|
|
90
|
+
else if (ch === '(') depth++
|
|
91
|
+
else if (ch === ')' && !--depth) return i
|
|
92
|
+
}
|
|
93
|
+
return -1
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Split at top-level `sep` (outside quotes, (), [], {}).
|
|
97
|
+
const splitTop = (text, sep) => {
|
|
98
|
+
const out = []
|
|
99
|
+
let depth = 0
|
|
100
|
+
let quote = null
|
|
101
|
+
let start = 0
|
|
102
|
+
for (let i = 0; i < text.length; i++) {
|
|
103
|
+
const ch = text[i]
|
|
104
|
+
if (quote) {
|
|
105
|
+
if (ch === '\\') i++
|
|
106
|
+
else if (ch === quote) quote = null
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
if (ch === '"' || ch === "'") quote = ch
|
|
110
|
+
else if (ch === '(' || ch === '[' || ch === '{') depth++
|
|
111
|
+
else if (ch === ')' || ch === ']' || ch === '}') depth = Math.max(0, depth - 1)
|
|
112
|
+
else if (ch === sep && !depth) {
|
|
113
|
+
out.push(text.slice(start, i))
|
|
114
|
+
start = i + 1
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
out.push(text.slice(start))
|
|
118
|
+
return out
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const IDENT_CHAR = /[\w\- -]/
|
|
122
|
+
const identEnd = (text, at) => {
|
|
123
|
+
let i = at
|
|
124
|
+
while (i < text.length) {
|
|
125
|
+
if (text[i] === '\\') i += 2
|
|
126
|
+
else if (IDENT_CHAR.test(text[i])) i++
|
|
127
|
+
else break
|
|
128
|
+
}
|
|
129
|
+
return i
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Selectors ───────────────────────────────────────────────────────────────
|
|
133
|
+
// Specificity of one complex selector, as one number (a, b, c → a·1e6 + b·1e3 + c).
|
|
134
|
+
const specificityOf = (sel) => {
|
|
135
|
+
let a = 0
|
|
136
|
+
let b = 0
|
|
137
|
+
let c = 0
|
|
138
|
+
const add = (n) => {
|
|
139
|
+
a += Math.floor(n / 1e6)
|
|
140
|
+
b += Math.floor(n / 1e3) % 1e3
|
|
141
|
+
c += n % 1e3
|
|
142
|
+
}
|
|
143
|
+
for (let i = 0; i < sel.length;) {
|
|
144
|
+
const ch = sel[i]
|
|
145
|
+
if (ch === '"' || ch === "'") {
|
|
146
|
+
const end = sel.indexOf(ch, i + 1)
|
|
147
|
+
i = end === -1 ? sel.length : end + 1
|
|
148
|
+
} else if (ch === '#') {
|
|
149
|
+
a++
|
|
150
|
+
i = identEnd(sel, i + 1)
|
|
151
|
+
} else if (ch === '.') {
|
|
152
|
+
b++
|
|
153
|
+
i = identEnd(sel, i + 1)
|
|
154
|
+
} else if (ch === '[') {
|
|
155
|
+
b++
|
|
156
|
+
const end = sel.indexOf(']', i)
|
|
157
|
+
i = end === -1 ? sel.length : end + 1
|
|
158
|
+
} else if (ch === ':') {
|
|
159
|
+
const element = sel[i + 1] === ':'
|
|
160
|
+
const start = i + (element ? 2 : 1)
|
|
161
|
+
const end = identEnd(sel, start)
|
|
162
|
+
const name = sel.slice(start, end).toLowerCase()
|
|
163
|
+
let args = null
|
|
164
|
+
let next = end
|
|
165
|
+
if (sel[end] === '(') {
|
|
166
|
+
const close = closeOf(sel, end)
|
|
167
|
+
args = sel.slice(end + 1, close === -1 ? sel.length : close)
|
|
168
|
+
next = close === -1 ? sel.length : close + 1
|
|
169
|
+
}
|
|
170
|
+
if (element || LEGACY_PSEUDO_ELEMENTS.has(name)) c++
|
|
171
|
+
else if (name === 'where') { /* 0 */ } else if (SELECTOR_LISTS.has(name) || name === 'not') {
|
|
172
|
+
if (args) add(Math.max(0, ...splitTop(args, ',').map((s) => specificityOf(s.trim()))))
|
|
173
|
+
} else b++
|
|
174
|
+
i = next
|
|
175
|
+
} else if (ch === '*' || ch === '>' || ch === '+' || ch === '~' || ch === '|' || /\s/.test(ch)) i++
|
|
176
|
+
else if (IDENT_CHAR.test(ch) || ch === '\\') {
|
|
177
|
+
c++
|
|
178
|
+
i = identEnd(sel, i)
|
|
179
|
+
} else i++
|
|
180
|
+
}
|
|
181
|
+
return a * 1e6 + b * 1e3 + c
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// The selector css-select can match: states and pseudo-elements become
|
|
185
|
+
// `:is(*)` (a no-op). `conditional` when a state, :lang() or a ::first-*
|
|
186
|
+
// was taken out; `target` 'element', 'placeholder' (::placeholder),
|
|
187
|
+
// 'before' / 'after', or null for what draws no text of the page's
|
|
188
|
+
// (::marker, ::selection, …).
|
|
189
|
+
const staticSelector = (sel) => {
|
|
190
|
+
let conditional = false
|
|
191
|
+
let target = 'element'
|
|
192
|
+
const walk = (text) => {
|
|
193
|
+
let out = ''
|
|
194
|
+
for (let i = 0; i < text.length;) {
|
|
195
|
+
const ch = text[i]
|
|
196
|
+
if (ch === '"' || ch === "'") {
|
|
197
|
+
const end = text.indexOf(ch, i + 1)
|
|
198
|
+
const stop = end === -1 ? text.length : end + 1
|
|
199
|
+
out += text.slice(i, stop)
|
|
200
|
+
i = stop
|
|
201
|
+
} else if (ch === '[') {
|
|
202
|
+
const end = text.indexOf(']', i)
|
|
203
|
+
const stop = end === -1 ? text.length : end + 1
|
|
204
|
+
out += text.slice(i, stop)
|
|
205
|
+
i = stop
|
|
206
|
+
} else if (ch === ':') {
|
|
207
|
+
const element = text[i + 1] === ':'
|
|
208
|
+
const start = i + (element ? 2 : 1)
|
|
209
|
+
const end = identEnd(text, start)
|
|
210
|
+
const name = text.slice(start, end).toLowerCase()
|
|
211
|
+
let args = null
|
|
212
|
+
let next = end
|
|
213
|
+
if (text[end] === '(') {
|
|
214
|
+
const close = closeOf(text, end)
|
|
215
|
+
args = text.slice(end + 1, close === -1 ? text.length : close)
|
|
216
|
+
next = close === -1 ? text.length : close + 1
|
|
217
|
+
}
|
|
218
|
+
if (element || LEGACY_PSEUDO_ELEMENTS.has(name)) {
|
|
219
|
+
if (name === 'placeholder' || name === '-webkit-input-placeholder' || name === '-moz-placeholder') target = 'placeholder'
|
|
220
|
+
else if (name === 'before' || name === 'after') target = name
|
|
221
|
+
else if (name === 'first-line' || name === 'first-letter') conditional = true
|
|
222
|
+
else target = null
|
|
223
|
+
out += ':is(*)'
|
|
224
|
+
} else if (name === 'not' && args !== null) {
|
|
225
|
+
const inner = staticSelectorList(args)
|
|
226
|
+
// a state inside :not() — drop the :not() (matches more, never less)
|
|
227
|
+
if (inner.conditional) {
|
|
228
|
+
conditional = true
|
|
229
|
+
out += ':is(*)'
|
|
230
|
+
} else out += `:not(${inner.text})`
|
|
231
|
+
} else if (SELECTOR_LISTS.has(name) && args !== null) {
|
|
232
|
+
const inner = staticSelectorList(args)
|
|
233
|
+
if (inner.conditional) conditional = true
|
|
234
|
+
out += `:${name === 'matches' || name.endsWith('-any') ? 'is' : name}(${inner.text || '*'})`
|
|
235
|
+
} else if (STATES.has(name) || name.startsWith('-webkit-') || name.startsWith('-moz-')) {
|
|
236
|
+
conditional = true
|
|
237
|
+
out += ':is(*)'
|
|
238
|
+
} else out += text.slice(i, next)
|
|
239
|
+
i = next
|
|
240
|
+
} else {
|
|
241
|
+
out += ch
|
|
242
|
+
i++
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return out
|
|
246
|
+
}
|
|
247
|
+
const text = walk(sel)
|
|
248
|
+
return { text, conditional, target }
|
|
249
|
+
}
|
|
250
|
+
// What is left when css-select still cannot match a selector: every
|
|
251
|
+
// pseudo-class out (it matches more, never less).
|
|
252
|
+
const looseSelector = (text) => {
|
|
253
|
+
let out = ''
|
|
254
|
+
for (let i = 0; i < text.length;) {
|
|
255
|
+
const ch = text[i]
|
|
256
|
+
if (ch === '"' || ch === "'" || ch === '[') {
|
|
257
|
+
const end = text.indexOf(ch === '[' ? ']' : ch, i + 1)
|
|
258
|
+
const stop = end === -1 ? text.length : end + 1
|
|
259
|
+
out += text.slice(i, stop)
|
|
260
|
+
i = stop
|
|
261
|
+
} else if (ch === ':') {
|
|
262
|
+
let end = identEnd(text, i + (text[i + 1] === ':' ? 2 : 1))
|
|
263
|
+
if (text[end] === '(') {
|
|
264
|
+
const close = closeOf(text, end)
|
|
265
|
+
end = close === -1 ? text.length : close + 1
|
|
266
|
+
}
|
|
267
|
+
out += ':is(*)'
|
|
268
|
+
i = end
|
|
269
|
+
} else {
|
|
270
|
+
out += ch
|
|
271
|
+
i++
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return out
|
|
275
|
+
}
|
|
276
|
+
const staticSelectorList = (list) => {
|
|
277
|
+
let conditional = false
|
|
278
|
+
const parts = splitTop(list, ',').map((s) => {
|
|
279
|
+
const r = staticSelector(s.trim())
|
|
280
|
+
if (r.conditional) conditional = true
|
|
281
|
+
return r.text || '*'
|
|
282
|
+
})
|
|
283
|
+
return { text: parts.join(', '), conditional }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ── Declarations ────────────────────────────────────────────────────────────
|
|
287
|
+
// The declarations of a block (its nested rules aside) that name a font
|
|
288
|
+
// property, `content` or a custom property.
|
|
289
|
+
const declarationsOf = (block) => {
|
|
290
|
+
const out = []
|
|
291
|
+
for (const part of splitTop(block.replace(/\/\*[\s\S]*?\*\//g, ''), ';')) {
|
|
292
|
+
if (part.includes('{')) continue
|
|
293
|
+
const colon = part.indexOf(':')
|
|
294
|
+
if (colon === -1) continue
|
|
295
|
+
const prop = part.slice(0, colon).trim()
|
|
296
|
+
const key = prop.startsWith('--') ? prop : prop.toLowerCase()
|
|
297
|
+
if (!FONT_PROPS.has(key) && key !== 'content' && !key.startsWith('--')) continue
|
|
298
|
+
let value = part.slice(colon + 1).trim()
|
|
299
|
+
const important = /!\s*important\s*$/i.test(value)
|
|
300
|
+
if (important) value = value.replace(/!\s*important\s*$/i, '').trim()
|
|
301
|
+
out.push({ prop: key, value, important })
|
|
302
|
+
}
|
|
303
|
+
return out
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Every style rule of a stylesheet text, at any depth of @media /
|
|
307
|
+
// @supports / @container / @layer (and CSS nesting), and of the files its
|
|
308
|
+
// @import rules name (`ctx.readSheet`): { selector, block, conditional }.
|
|
309
|
+
// `ctx.unknown` turns true for a sheet that cannot be read.
|
|
310
|
+
const IMPORT = /^@import\s+(?:url\(\s*)?(['"]?)([^'")\s]+)\1\s*\)?\s*([^;]*)/i
|
|
311
|
+
const styleRulesOf = (css, conditional, ctx, from, parent = null, into = []) => {
|
|
312
|
+
const rules = stylesheetRules(css)
|
|
313
|
+
if (!rules) {
|
|
314
|
+
ctx.unknown = true
|
|
315
|
+
return into
|
|
316
|
+
}
|
|
317
|
+
for (const rule of rules) {
|
|
318
|
+
const open = rule.indexOf('{')
|
|
319
|
+
if (open === -1) {
|
|
320
|
+
const m = IMPORT.exec(rule)
|
|
321
|
+
if (!m) continue
|
|
322
|
+
const sheet = ctx.readSheet ? ctx.readSheet(m[2], from) : null
|
|
323
|
+
if (typeof sheet !== 'string') ctx.unknown = true
|
|
324
|
+
else styleRulesOf(sheet, conditional || (!!m[3].trim() && !ALWAYS_MEDIA.test(m[3])), ctx, m[2], parent, into)
|
|
325
|
+
continue
|
|
326
|
+
}
|
|
327
|
+
if (!rule.endsWith('}')) continue
|
|
328
|
+
const prelude = rule.slice(0, open).trim()
|
|
329
|
+
const body = rule.slice(open + 1, -1)
|
|
330
|
+
if (prelude[0] === '@') {
|
|
331
|
+
const name = (/^@([\w-]+)/.exec(prelude) || [])[1]?.toLowerCase()
|
|
332
|
+
if (name === 'media') styleRulesOf(body, conditional || !ALWAYS_MEDIA.test(prelude.slice(6)), ctx, from, parent, into)
|
|
333
|
+
// a layer reorders the cascade: its rules count as possibilities
|
|
334
|
+
else if (name === 'supports' || name === 'container' || name === 'document' || name === 'scope' || name === 'starting-style' || name === 'layer') {
|
|
335
|
+
styleRulesOf(body, true, ctx, from, parent, into)
|
|
336
|
+
}
|
|
337
|
+
continue
|
|
338
|
+
}
|
|
339
|
+
const selector = parent
|
|
340
|
+
? splitTop(prelude, ',').map((s) => s.includes('&') ? s.replace(/&/g, `:is(${parent})`) : `:is(${parent}) ${s.trim()}`).join(', ')
|
|
341
|
+
: prelude
|
|
342
|
+
into.push({ selector, block: body, conditional })
|
|
343
|
+
// nested rules (CSS nesting)
|
|
344
|
+
if (body.includes('{')) {
|
|
345
|
+
const nested = body.slice(body.search(/[^;{}]*\{/))
|
|
346
|
+
styleRulesOf(nested, conditional, ctx, from, selector, into)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return into
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ── Values ──────────────────────────────────────────────────────────────────
|
|
353
|
+
// var() in `value`, from `vars` (name → values): every resulting text (at
|
|
354
|
+
// most MAX_VALUES); null when a var() has no value and no fallback.
|
|
355
|
+
const substitute = (value, vars, depth = 0) => {
|
|
356
|
+
const at = value.indexOf('var(')
|
|
357
|
+
if (at === -1) return [value]
|
|
358
|
+
if (depth > 8) return []
|
|
359
|
+
const close = closeOf(value, at + 3)
|
|
360
|
+
if (close === -1) return []
|
|
361
|
+
const inner = value.slice(at + 4, close)
|
|
362
|
+
const comma = splitTop(inner, ',')
|
|
363
|
+
const name = comma[0].trim()
|
|
364
|
+
const fallback = comma.length > 1 ? comma.slice(1).join(',').trim() : null
|
|
365
|
+
const known = vars.get(name)
|
|
366
|
+
const options = known && known.length ? known : fallback !== null ? substitute(fallback, vars, depth + 1) : []
|
|
367
|
+
const out = []
|
|
368
|
+
for (const option of options) {
|
|
369
|
+
for (const rest of substitute(value.slice(0, at) + option + value.slice(close + 1), vars, depth + 1)) {
|
|
370
|
+
if (!out.includes(rest)) out.push(rest)
|
|
371
|
+
if (out.length >= MAX_VALUES) return out
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return out
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const unquote = (name) => name.trim().replace(/^(['"])([\s\S]*)\1$/, '$2').replace(/\\(.)/g, '$1').trim().toLowerCase()
|
|
378
|
+
const familiesOf = (value) => splitTop(value, ',').map(unquote).filter(Boolean)
|
|
379
|
+
|
|
380
|
+
const WEIGHT_WORDS = { normal: 400, bold: 700 }
|
|
381
|
+
const weightOf = (value, parent) => {
|
|
382
|
+
const v = String(value).trim().toLowerCase()
|
|
383
|
+
if (v in WEIGHT_WORDS) return WEIGHT_WORDS[v]
|
|
384
|
+
if (v === 'bolder') return parent < 350 ? 400 : parent < 550 ? 700 : 900
|
|
385
|
+
if (v === 'lighter') return parent < 550 ? 100 : parent < 750 ? 400 : 700
|
|
386
|
+
const n = Number(v)
|
|
387
|
+
return Number.isFinite(n) && n >= 1 && n <= 1000 ? n : null
|
|
388
|
+
}
|
|
389
|
+
const styleOf = (value) => {
|
|
390
|
+
const v = String(value).trim().toLowerCase().split(/\s+/)[0]
|
|
391
|
+
return v === 'italic' || v === 'oblique' || v === 'normal' ? v : null
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// The text a `content` value draws: its strings (CSS escapes read) and
|
|
395
|
+
// attr() values; '' for none, normal, url(), counters and quotes.
|
|
396
|
+
const CSS_ESCAPE = /\\([0-9a-f]{1,6})\s?|\\([\s\S])/gi
|
|
397
|
+
const contentText = (value, el) => {
|
|
398
|
+
let out = ''
|
|
399
|
+
for (const m of value.matchAll(/(["'])((?:\\[\s\S]|(?!\1)[^\\])*)\1|attr\(\s*([\w-]+)[^)]*\)/gi)) {
|
|
400
|
+
if (m[3]) out += el.getAttribute(m[3]) || ''
|
|
401
|
+
else {
|
|
402
|
+
out += m[2].replace(CSS_ESCAPE, (all, hex, ch) => {
|
|
403
|
+
if (!hex) return ch
|
|
404
|
+
const code = parseInt(hex, 16)
|
|
405
|
+
return code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : '\ufffd'
|
|
406
|
+
})
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return out
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const SYSTEM_FONTS = new Set(['caption', 'icon', 'menu', 'message-box', 'small-caption', 'status-bar'])
|
|
413
|
+
// The `font` shorthand → { family, weight, style } (null: not a font).
|
|
414
|
+
const fontShorthand = (value) => {
|
|
415
|
+
const v = value.trim()
|
|
416
|
+
if (SYSTEM_FONTS.has(v.toLowerCase())) return { family: 'system-ui', weight: 'normal', style: 'normal' }
|
|
417
|
+
const tokens = []
|
|
418
|
+
let rest = v
|
|
419
|
+
// the tokens before the size: style, variant, weight, stretch
|
|
420
|
+
for (;;) {
|
|
421
|
+
const m = /^\s*([^\s,'"]+)/.exec(rest)
|
|
422
|
+
if (!m) break
|
|
423
|
+
const word = m[1]
|
|
424
|
+
if (/^[\d.]/.test(word) && !/^\d{1,4}$/.test(word)) break // a size (12px, 1.2em, 100%)
|
|
425
|
+
if (/^\d{1,4}$/.test(word) && Number(word) > 1000) break
|
|
426
|
+
const low = word.toLowerCase()
|
|
427
|
+
if (/^(xx-small|x-small|small|medium|large|x-large|xx-large|xxx-large|larger|smaller|calc\(|clamp\(|min\(|max\()/.test(low)) break
|
|
428
|
+
tokens.push(low)
|
|
429
|
+
rest = rest.slice(m[0].length)
|
|
430
|
+
if (tokens.length > 4) break
|
|
431
|
+
}
|
|
432
|
+
// the size [/ line-height], then the families
|
|
433
|
+
const size = /^\s*(?:[^\s/]+|(?:calc|clamp|min|max)\([^)]*\))(?:\s*\/\s*[^\s]+)?\s+/.exec(rest)
|
|
434
|
+
if (!size) return null
|
|
435
|
+
const family = rest.slice(size[0].length).trim()
|
|
436
|
+
if (!family) return null
|
|
437
|
+
let weight = 'normal'
|
|
438
|
+
let style = 'normal'
|
|
439
|
+
for (const t of tokens) {
|
|
440
|
+
if (t === 'italic' || t === 'oblique') style = t
|
|
441
|
+
else if (t === 'bold' || t === 'bolder' || t === 'lighter' || /^\d{1,4}$/.test(t)) weight = t
|
|
442
|
+
}
|
|
443
|
+
return { family, weight, style }
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ── The faces sheet ─────────────────────────────────────────────────────────
|
|
447
|
+
const descriptor = (body, name) => {
|
|
448
|
+
const m = new RegExp(`(?:^|;|\\s)${name}\\s*:\\s*([^;]+)`, 'i').exec(body)
|
|
449
|
+
return m ? m[1].trim() : null
|
|
450
|
+
}
|
|
451
|
+
const rangesOf = (text) => {
|
|
452
|
+
if (!text) return null
|
|
453
|
+
const out = []
|
|
454
|
+
for (const part of String(text).split(',')) {
|
|
455
|
+
const p = part.trim().replace(/^u\+/i, '')
|
|
456
|
+
if (!p) continue
|
|
457
|
+
if (p.includes('?')) {
|
|
458
|
+
out.push([parseInt(p.replace(/\?/g, '0'), 16), parseInt(p.replace(/\?/g, 'f'), 16)])
|
|
459
|
+
continue
|
|
460
|
+
}
|
|
461
|
+
const [x, y] = p.split('-')
|
|
462
|
+
const from = parseInt(x, 16)
|
|
463
|
+
out.push([from, y ? parseInt(y, 16) : from])
|
|
464
|
+
}
|
|
465
|
+
return out.length ? out : null
|
|
466
|
+
}
|
|
467
|
+
const facesOf = (sheet) => {
|
|
468
|
+
const faces = []
|
|
469
|
+
let index = 0
|
|
470
|
+
for (const m of sheet.matchAll(FACE_RULE)) {
|
|
471
|
+
const body = m[1]
|
|
472
|
+
const family = unquote(descriptor(body, 'font-family') || '')
|
|
473
|
+
const weights = String(descriptor(body, 'font-weight') || '400').trim().split(/\s+/).map((w) => weightOf(w, 400) || 400)
|
|
474
|
+
faces.push({
|
|
475
|
+
index: index++,
|
|
476
|
+
family,
|
|
477
|
+
min: Math.min(...weights),
|
|
478
|
+
max: Math.max(...weights),
|
|
479
|
+
style: styleOf(descriptor(body, 'font-style') || 'normal') || 'normal',
|
|
480
|
+
ranges: rangesOf(descriptor(body, 'unicode-range'))
|
|
481
|
+
})
|
|
482
|
+
}
|
|
483
|
+
return faces
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// The browser's face matching (CSS Fonts 4, §5.2): style first, then weight.
|
|
487
|
+
const STYLE_ORDER = { italic: ['italic', 'oblique', 'normal'], oblique: ['oblique', 'italic', 'normal'], normal: ['normal', 'oblique', 'italic'] }
|
|
488
|
+
const byStyle = (faces, style) => {
|
|
489
|
+
for (const s of STYLE_ORDER[style] || STYLE_ORDER.normal) {
|
|
490
|
+
const got = faces.filter((f) => f.style === s)
|
|
491
|
+
if (got.length) return got
|
|
492
|
+
}
|
|
493
|
+
return faces
|
|
494
|
+
}
|
|
495
|
+
const byWeight = (faces, w) => {
|
|
496
|
+
const within = faces.filter((f) => f.min <= w && w <= f.max)
|
|
497
|
+
if (within.length) return within
|
|
498
|
+
const below = faces.filter((f) => f.max < w).sort((x, y) => y.max - x.max)
|
|
499
|
+
const above = faces.filter((f) => f.min > w).sort((x, y) => x.min - y.min)
|
|
500
|
+
let pick
|
|
501
|
+
if (w >= 400 && w <= 500) pick = above.find((f) => f.min <= 500) || below[0] || above[0]
|
|
502
|
+
else if (w < 400) pick = below[0] || above[0]
|
|
503
|
+
else pick = above[0] || below[0]
|
|
504
|
+
return pick ? faces.filter((f) => f.min === pick.min && f.max === pick.max) : []
|
|
505
|
+
}
|
|
506
|
+
const covers = (face, code) => !face.ranges || face.ranges.some(([from, to]) => code >= from && code <= to)
|
|
507
|
+
|
|
508
|
+
// ── The page ────────────────────────────────────────────────────────────────
|
|
509
|
+
// The cascade's order of origins: the browser's rules, the page's, the
|
|
510
|
+
// page's !important ones (the browser's !important ones are not read).
|
|
511
|
+
const levelOf = (d) => (d.ua ? 0 : d.important ? 2 : 1)
|
|
512
|
+
const beats = (x, y) => {
|
|
513
|
+
const lx = levelOf(x)
|
|
514
|
+
const ly = levelOf(y)
|
|
515
|
+
if (lx !== ly) return lx > ly
|
|
516
|
+
if (x.rank !== y.rank) return x.rank > y.rank
|
|
517
|
+
return x.order > y.order
|
|
518
|
+
}
|
|
519
|
+
// The values that may win: the winner of the rules that always hold, and
|
|
520
|
+
// every conditional one that would beat it; `inherit` when no rule always
|
|
521
|
+
// holds (the inherited value stays possible).
|
|
522
|
+
const winnersOf = (list) => {
|
|
523
|
+
let base = null
|
|
524
|
+
for (const d of list) if (!d.conditional && (!base || beats(d, base))) base = d
|
|
525
|
+
const out = base ? [base] : []
|
|
526
|
+
for (const d of list) if (d.conditional && (!base || beats(d, base))) out.push(d)
|
|
527
|
+
return { list: out, inherit: !base }
|
|
528
|
+
}
|
|
529
|
+
const uniq = (values) => {
|
|
530
|
+
const out = []
|
|
531
|
+
const keys = new Set()
|
|
532
|
+
for (const v of values) {
|
|
533
|
+
const key = JSON.stringify(v)
|
|
534
|
+
if (keys.has(key)) continue
|
|
535
|
+
keys.add(key)
|
|
536
|
+
out.push(v)
|
|
537
|
+
if (out.length >= MAX_VALUES) break
|
|
538
|
+
}
|
|
539
|
+
return out
|
|
540
|
+
}
|
|
541
|
+
const varsIn = (text, into) => {
|
|
542
|
+
VAR_NAME.lastIndex = 0
|
|
543
|
+
let m
|
|
544
|
+
while ((m = VAR_NAME.exec(text))) into.add(m[1])
|
|
545
|
+
return into
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const ROOT = { vars: new Map(), family: [['serif']], weight: [400], style: ['normal'] }
|
|
549
|
+
const SKIP = new Set(['script', 'style', 'template', 'noscript', 'head', 'title'])
|
|
550
|
+
|
|
551
|
+
// One element's possible fonts, from its declarations and its parent's.
|
|
552
|
+
const fontOf = (decls, parent) => {
|
|
553
|
+
// custom properties (inherited): the raw values first, then var() in them
|
|
554
|
+
const vars = new Map(parent.vars)
|
|
555
|
+
const names = [...new Set(decls.filter((d) => d.prop.startsWith('--')).map((d) => d.prop))]
|
|
556
|
+
for (const name of names) {
|
|
557
|
+
const { list, inherit } = winnersOf(decls.filter((d) => d.prop === name))
|
|
558
|
+
const values = inherit ? [...(parent.vars.get(name) || [])] : []
|
|
559
|
+
for (const d of list) {
|
|
560
|
+
const v = d.value.trim()
|
|
561
|
+
if (/^(inherit|unset)$/i.test(v)) values.push(...(parent.vars.get(name) || []))
|
|
562
|
+
else if (!/^(initial|revert|revert-layer)$/i.test(v)) values.push(v)
|
|
563
|
+
}
|
|
564
|
+
vars.set(name, uniq(values))
|
|
565
|
+
}
|
|
566
|
+
for (const name of names) vars.set(name, uniq(vars.get(name).flatMap((v) => substitute(v, vars))))
|
|
567
|
+
const font = { vars }
|
|
568
|
+
// font-family, font-weight, font-style (the `font` shorthand sets all three)
|
|
569
|
+
for (const prop of ['family', 'weight', 'style']) {
|
|
570
|
+
const longhand = 'font-' + prop
|
|
571
|
+
const { list, inherit } = winnersOf(decls.filter((d) => d.prop === longhand || d.prop === 'font'))
|
|
572
|
+
const values = inherit ? [...parent[prop]] : []
|
|
573
|
+
for (const d of list) {
|
|
574
|
+
const before = values.length
|
|
575
|
+
for (const text of substitute(d.value, vars)) {
|
|
576
|
+
const keyword = text.trim().toLowerCase()
|
|
577
|
+
if (keyword === 'inherit' || keyword === 'unset') {
|
|
578
|
+
values.push(...parent[prop])
|
|
579
|
+
continue
|
|
580
|
+
}
|
|
581
|
+
if (keyword === 'initial' || keyword === 'revert' || keyword === 'revert-layer') {
|
|
582
|
+
values.push(...ROOT[prop])
|
|
583
|
+
continue
|
|
584
|
+
}
|
|
585
|
+
let raw = text
|
|
586
|
+
if (d.prop === 'font') {
|
|
587
|
+
const f = fontShorthand(text)
|
|
588
|
+
if (!f) continue
|
|
589
|
+
raw = f[prop]
|
|
590
|
+
}
|
|
591
|
+
if (prop === 'family') values.push(familiesOf(raw))
|
|
592
|
+
else if (prop === 'style') {
|
|
593
|
+
const st = styleOf(raw)
|
|
594
|
+
if (st) values.push(st)
|
|
595
|
+
} else {
|
|
596
|
+
for (const p of parent.weight) {
|
|
597
|
+
const w = weightOf(raw, p)
|
|
598
|
+
if (w) values.push(w)
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
// A value that is no value (a var() with none, a word that is not a
|
|
603
|
+
// font) leaves the property inherited.
|
|
604
|
+
if (values.length === before) values.push(...parent[prop])
|
|
605
|
+
}
|
|
606
|
+
font[prop] = uniq(values)
|
|
607
|
+
}
|
|
608
|
+
return font
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* The faces of the page's faces sheet that its elements draw.
|
|
613
|
+
* @param {string} html - a page built ahead (its faces sheet in place)
|
|
614
|
+
* @param {{ readSheet?: (href: string, from?: string) => string | null }} [options]
|
|
615
|
+
* @returns {Map<number, string> | null} faces sheet rule index → the characters
|
|
616
|
+
* drawn in it; null when a stylesheet of the page cannot be read
|
|
617
|
+
*/
|
|
618
|
+
export const drawnFaces = (html, options = {}) => {
|
|
619
|
+
const drawn = new Map()
|
|
620
|
+
const sheet = FACES_STYLE.exec(html)
|
|
621
|
+
if (!sheet) return drawn
|
|
622
|
+
const faces = facesOf(sheet[1])
|
|
623
|
+
if (!faces.length) return drawn
|
|
624
|
+
const families = new Map()
|
|
625
|
+
for (const f of faces) {
|
|
626
|
+
if (!families.has(f.family)) families.set(f.family, [])
|
|
627
|
+
families.get(f.family).push(f)
|
|
628
|
+
}
|
|
629
|
+
// Scripts, templates and the faces sheet hold no text the page draws.
|
|
630
|
+
const lean = html.replace(FACES_STYLE, '<style data-smbls-faces></style>').replace(RAW, '<$1$2></$1>')
|
|
631
|
+
const { document } = parseHTML(lean)
|
|
632
|
+
|
|
633
|
+
// The rules: the browser's, then the page's sheets in document order,
|
|
634
|
+
// then the style attributes (above every rule).
|
|
635
|
+
const ctx = { readSheet: typeof options.readSheet === 'function' ? options.readSheet : null, unknown: false }
|
|
636
|
+
const rules = []
|
|
637
|
+
const add = (css, conditional, ua, from) => {
|
|
638
|
+
for (const rule of styleRulesOf(css, conditional, ctx, from)) {
|
|
639
|
+
const decls = declarationsOf(rule.block)
|
|
640
|
+
if (decls.length) rules.push({ selector: rule.selector, decls, conditional: rule.conditional, ua })
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
add(UA_RULES, false, true)
|
|
644
|
+
for (const el of document.querySelectorAll('style, link')) {
|
|
645
|
+
const media = el.getAttribute('media')
|
|
646
|
+
const conditional = !!media && !ALWAYS_MEDIA.test(media)
|
|
647
|
+
if (el.localName === 'style') {
|
|
648
|
+
if (!el.hasAttribute('data-smbls-faces')) add(el.textContent || '', conditional, false)
|
|
649
|
+
continue
|
|
650
|
+
}
|
|
651
|
+
const rel = (el.getAttribute('rel') || '').toLowerCase().split(/\s+/)
|
|
652
|
+
if (!rel.includes('stylesheet') || el.hasAttribute('data-smbls-font') || el.hasAttribute('disabled')) continue
|
|
653
|
+
const href = el.getAttribute('href') || ''
|
|
654
|
+
const css = ctx.readSheet && href ? ctx.readSheet(href) : null
|
|
655
|
+
if (typeof css !== 'string') return null
|
|
656
|
+
add(css, conditional || rel.includes('alternate'), false, href)
|
|
657
|
+
}
|
|
658
|
+
if (ctx.unknown) return null
|
|
659
|
+
const attributed = []
|
|
660
|
+
for (const el of document.querySelectorAll('[style]')) {
|
|
661
|
+
const decls = declarationsOf(el.getAttribute('style') || '')
|
|
662
|
+
if (decls.length) attributed.push({ el, decls })
|
|
663
|
+
}
|
|
664
|
+
// Only the custom properties a font or `content` declaration reads (at
|
|
665
|
+
// any depth).
|
|
666
|
+
const wanted = new Set()
|
|
667
|
+
const every = [...rules.flatMap((r) => r.decls), ...attributed.flatMap((a) => a.decls)]
|
|
668
|
+
for (const d of every) if (FONT_PROPS.has(d.prop) || d.prop === 'content') varsIn(d.value, wanted)
|
|
669
|
+
for (let size = -1; size !== wanted.size;) {
|
|
670
|
+
size = wanted.size
|
|
671
|
+
for (const d of every) if (wanted.has(d.prop)) varsIn(d.value, wanted)
|
|
672
|
+
}
|
|
673
|
+
const keep = (d) => FONT_PROPS.has(d.prop) || d.prop === 'content' || wanted.has(d.prop)
|
|
674
|
+
|
|
675
|
+
// Per element (and per ::placeholder, ::before, ::after of it): its
|
|
676
|
+
// declarations of those properties, each with its place in the cascade.
|
|
677
|
+
const declared = { element: new Map(), placeholder: new Map(), before: new Map(), after: new Map() }
|
|
678
|
+
let order = 0
|
|
679
|
+
for (const rule of rules) {
|
|
680
|
+
const decls = rule.decls.filter(keep)
|
|
681
|
+
if (!decls.length) continue
|
|
682
|
+
for (const part of splitTop(rule.selector, ',')) {
|
|
683
|
+
const selector = part.trim()
|
|
684
|
+
if (!selector) continue
|
|
685
|
+
let { text, conditional, target } = staticSelector(selector)
|
|
686
|
+
if (!target) continue
|
|
687
|
+
let matched
|
|
688
|
+
try {
|
|
689
|
+
matched = document.querySelectorAll(text)
|
|
690
|
+
} catch (err) {
|
|
691
|
+
// matched with its pseudo-classes out: a possibility, never certain
|
|
692
|
+
try { matched = document.querySelectorAll(looseSelector(text)) } catch (err2) { continue }
|
|
693
|
+
conditional = true
|
|
694
|
+
}
|
|
695
|
+
const rank = rule.ua ? 0 : specificityOf(selector)
|
|
696
|
+
const entries = decls.map((d) => ({ ...d, rank, order: ++order, conditional: conditional || rule.conditional, ua: rule.ua }))
|
|
697
|
+
const into = declared[target]
|
|
698
|
+
for (const el of matched) {
|
|
699
|
+
if (!into.has(el)) into.set(el, [])
|
|
700
|
+
into.get(el).push(...entries)
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
for (const { el, decls } of attributed) {
|
|
705
|
+
const own = decls.filter(keep)
|
|
706
|
+
if (!own.length) continue
|
|
707
|
+
if (!declared.element.has(el)) declared.element.set(el, [])
|
|
708
|
+
declared.element.get(el).push(...own.map((d) => ({ ...d, rank: 1e9, order: ++order, conditional: false, ua: false })))
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// The characters of each element's own text, per font it may be drawn in.
|
|
712
|
+
const textOf = new Map()
|
|
713
|
+
const addText = (font, text) => {
|
|
714
|
+
if (!text.trim()) return
|
|
715
|
+
for (const family of font.family) {
|
|
716
|
+
for (const weight of font.weight) {
|
|
717
|
+
for (const style of font.style) {
|
|
718
|
+
const key = JSON.stringify([family, weight, style])
|
|
719
|
+
textOf.set(key, (textOf.get(key) || '') + text)
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
const walk = (el, parent) => {
|
|
725
|
+
const font = fontOf(declared.element.get(el) || [], parent)
|
|
726
|
+
let own = ''
|
|
727
|
+
for (const node of el.childNodes) {
|
|
728
|
+
if (node.nodeType === 3) own += node.textContent
|
|
729
|
+
else if (node.nodeType === 1 && !SKIP.has(node.localName)) walk(node, font)
|
|
730
|
+
}
|
|
731
|
+
const tag = el.localName
|
|
732
|
+
if (tag === 'input' || tag === 'textarea') {
|
|
733
|
+
own += ' ' + (el.getAttribute('value') || '')
|
|
734
|
+
// ::placeholder inherits from its field
|
|
735
|
+
const placeholder = el.getAttribute('placeholder')
|
|
736
|
+
if (placeholder) addText(declared.placeholder.has(el) ? fontOf(declared.placeholder.get(el), font) : font, placeholder)
|
|
737
|
+
}
|
|
738
|
+
addText(font, own)
|
|
739
|
+
// ::before / ::after inherit from the element; their text is `content`
|
|
740
|
+
for (const kind of ['before', 'after']) {
|
|
741
|
+
const decls = declared[kind].get(el)
|
|
742
|
+
if (!decls) continue
|
|
743
|
+
const box = fontOf(decls, font)
|
|
744
|
+
for (const d of winnersOf(decls.filter((x) => x.prop === 'content')).list) {
|
|
745
|
+
for (const value of substitute(d.value, box.vars)) addText(box, contentText(value, el))
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (document.documentElement) walk(document.documentElement, ROOT)
|
|
750
|
+
|
|
751
|
+
// The faces those characters are drawn in: per family, the faces of the
|
|
752
|
+
// style and weight the browser picks; per character, the last of them
|
|
753
|
+
// whose unicode-range holds it (CSS Fonts 4: overlapping ranges, the last
|
|
754
|
+
// rule first), else the next family.
|
|
755
|
+
for (const [key, text] of textOf) {
|
|
756
|
+
const [list, weight, style] = JSON.parse(key)
|
|
757
|
+
const chosen = new Map()
|
|
758
|
+
for (const family of list) {
|
|
759
|
+
if (GENERIC.has(family)) break
|
|
760
|
+
const all = families.get(family)
|
|
761
|
+
if (all && !chosen.has(family)) chosen.set(family, byWeight(byStyle(all, style), weight))
|
|
762
|
+
}
|
|
763
|
+
if (!chosen.size) continue
|
|
764
|
+
const seen = new Set()
|
|
765
|
+
for (const ch of text) {
|
|
766
|
+
if (seen.has(ch)) continue
|
|
767
|
+
seen.add(ch)
|
|
768
|
+
const code = ch.codePointAt(0)
|
|
769
|
+
if (code <= 32 || code === 0xa0) continue
|
|
770
|
+
for (const family of list) {
|
|
771
|
+
if (GENERIC.has(family)) break
|
|
772
|
+
const set = chosen.get(family) || []
|
|
773
|
+
let face = null
|
|
774
|
+
for (let i = set.length - 1; i >= 0 && !face; i--) if (covers(set[i], code)) face = set[i]
|
|
775
|
+
if (!face) continue
|
|
776
|
+
const chars = drawn.get(face.index) || ''
|
|
777
|
+
if (!chars.includes(ch)) drawn.set(face.index, chars + ch)
|
|
778
|
+
break
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return drawn
|
|
783
|
+
}
|
package/index.js
CHANGED
|
@@ -16,6 +16,9 @@ import { trimSpriteToPage, classRulesForPage, markupClasses, trimVariablesToPage
|
|
|
16
16
|
// page's faces name (`smbls build` maps file: URLs into its output) marks the
|
|
17
17
|
// sheet again.
|
|
18
18
|
import { markFacesSheet, facesSheetMarks } from './faces.js'
|
|
19
|
+
// The faces a page's elements draw (XMA r12 results, item 2): `smbls build`
|
|
20
|
+
// inlines and awaits only those.
|
|
21
|
+
import { drawnFaces } from './drawnFaces.js'
|
|
19
22
|
|
|
20
23
|
export {
|
|
21
24
|
createEnv,
|
|
@@ -48,7 +51,8 @@ export {
|
|
|
48
51
|
trimVariablesToPage,
|
|
49
52
|
stylesheetRules,
|
|
50
53
|
markFacesSheet,
|
|
51
|
-
facesSheetMarks
|
|
54
|
+
facesSheetMarks,
|
|
55
|
+
drawnFaces
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
export default {
|
|
@@ -82,5 +86,6 @@ export default {
|
|
|
82
86
|
trimVariablesToPage,
|
|
83
87
|
stylesheetRules,
|
|
84
88
|
markFacesSheet,
|
|
85
|
-
facesSheetMarks
|
|
89
|
+
facesSheetMarks,
|
|
90
|
+
drawnFaces
|
|
86
91
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbo.ls/brender",
|
|
3
|
-
"version": "3.14.
|
|
3
|
+
"version": "3.14.679",
|
|
4
4
|
"license": "CC-BY-NC-4.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./dist/esm/index.js",
|
|
@@ -30,6 +30,11 @@
|
|
|
30
30
|
"development": "./faces.js",
|
|
31
31
|
"import": "./dist/esm/faces.js",
|
|
32
32
|
"default": "./dist/esm/faces.js"
|
|
33
|
+
},
|
|
34
|
+
"./drawnFaces": {
|
|
35
|
+
"development": "./drawnFaces.js",
|
|
36
|
+
"import": "./dist/esm/drawnFaces.js",
|
|
37
|
+
"default": "./dist/esm/drawnFaces.js"
|
|
33
38
|
}
|
|
34
39
|
},
|
|
35
40
|
"source": "index.js",
|
|
@@ -53,16 +58,16 @@
|
|
|
53
58
|
"dependencies": {
|
|
54
59
|
"@symbo.ls/css": "^3.14.782",
|
|
55
60
|
"@symbo.ls/default-config": "^3.14.789",
|
|
56
|
-
"@symbo.ls/element": "^3.14.
|
|
57
|
-
"@symbo.ls/fetch": "^3.14.
|
|
58
|
-
"@symbo.ls/helmet": "^3.14.
|
|
61
|
+
"@symbo.ls/element": "^3.14.819",
|
|
62
|
+
"@symbo.ls/fetch": "^3.14.675",
|
|
63
|
+
"@symbo.ls/helmet": "^3.14.674",
|
|
59
64
|
"@symbo.ls/scratch": "^3.14.796",
|
|
60
|
-
"@symbo.ls/utils": "^3.14.
|
|
65
|
+
"@symbo.ls/utils": "^3.14.855",
|
|
61
66
|
"esbuild": "^0.28.1",
|
|
62
67
|
"linkedom": "^0.16.8"
|
|
63
68
|
},
|
|
64
69
|
"peerDependencies": {
|
|
65
|
-
"smbls": "^3.14.
|
|
70
|
+
"smbls": "^3.14.862"
|
|
66
71
|
},
|
|
67
72
|
"optionalDependencies": {
|
|
68
73
|
"@symbo.ls/funcql": "^3.14.0"
|