single-file-core 1.5.116 → 1.5.117
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/core/helper.js +22 -6
- package/core/lib/processor-helper-inline.js +13 -0
- package/core/lib/processor-helper.js +19 -1
- package/modules/css-fonts-minifier.js +86 -18
- package/package.json +1 -1
- package/processors/compression/compression-packager.js +2 -11
- package/processors/compression/compression.js +21 -8
- package/test/sfz-harness/css-fonts-minifier.js +62 -11
- package/test/sfz-harness/format-rules.js +3 -1
package/core/helper.js
CHANGED
|
@@ -408,10 +408,10 @@ function getElementsInfo(win, doc, element, options, data = { usedFonts: new Map
|
|
|
408
408
|
}
|
|
409
409
|
}
|
|
410
410
|
if (options.removeUnusedFonts) {
|
|
411
|
-
getUsedFont(computedStyle,
|
|
412
|
-
getUsedFont(getComputedStyle(win, element, ":first-letter"),
|
|
413
|
-
getUsedFont(getComputedStyle(win, element, ":before"),
|
|
414
|
-
getUsedFont(getComputedStyle(win, element, ":after"),
|
|
411
|
+
getUsedFont(computedStyle, data.usedFonts);
|
|
412
|
+
getUsedFont(getComputedStyle(win, element, ":first-letter"), data.usedFonts);
|
|
413
|
+
getUsedFont(getComputedStyle(win, element, ":before"), data.usedFonts);
|
|
414
|
+
getUsedFont(getComputedStyle(win, element, ":after"), data.usedFonts);
|
|
415
415
|
}
|
|
416
416
|
}
|
|
417
417
|
}
|
|
@@ -611,12 +611,28 @@ function getResourcesInfo(win, doc, element, options, data, elementHidden, compu
|
|
|
611
611
|
}
|
|
612
612
|
}
|
|
613
613
|
|
|
614
|
-
|
|
614
|
+
// Every family in the computed font-family is recorded, and the set of faces the document actually
|
|
615
|
+
// resolved is deliberately NOT consulted. Narrowing the list to the loaded faces is the obvious
|
|
616
|
+
// idea — a fallback stack names every family the page might need and only some are used — and it
|
|
617
|
+
// was written in 2019, wired to an option nothing ever set, and dead until it was reconnected here.
|
|
618
|
+
// Reconnected, it saved nothing measurable on nine real pages and cost fidelity twice: a face
|
|
619
|
+
// matched on its declared style is not found when the browser SYNTHESIZED that style, so a family
|
|
620
|
+
// drawn in an italic it declares no face for was dropped from the page that draws it; and a webfont
|
|
621
|
+
// that merely failed to load on the capturing machine would be dropped from the archive for good.
|
|
622
|
+
// It belongs with the unicode-range work, which needs the same information and can weigh a measured
|
|
623
|
+
// benefit against those, not here.
|
|
624
|
+
function getUsedFont(computedStyle, usedFonts) {
|
|
615
625
|
if (computedStyle) {
|
|
616
626
|
const fontStyle = computedStyle.getPropertyValue("font-style") || "normal";
|
|
617
627
|
computedStyle.getPropertyValue("font-family").split(",").forEach(fontFamilyName => {
|
|
618
628
|
fontFamilyName = normalizeFontFamily(fontFamilyName);
|
|
619
|
-
|
|
629
|
+
// an element with no computed font-family at all is one whose styles could not be read,
|
|
630
|
+
// not one drawn in a family with no name. It happens to every element of a frame that
|
|
631
|
+
// was re-parsed from its srcdoc, because the computed style is asked of the parent
|
|
632
|
+
// window and the document it belongs to is not the one being rendered. Recorded, it
|
|
633
|
+
// turns "nothing is known here" into a list of length one, which reads downstream as a
|
|
634
|
+
// complete answer and prunes every face the frame declares
|
|
635
|
+
if (fontFamilyName) {
|
|
620
636
|
const fontWeight = getFontWeight(computedStyle.getPropertyValue("font-weight"));
|
|
621
637
|
const fontVariant = computedStyle.getPropertyValue("font-variant") || "normal";
|
|
622
638
|
const value = [fontFamilyName, fontWeight, fontStyle, fontVariant];
|
|
@@ -44,6 +44,10 @@ const EMPTY_URL_SOURCE = /^url\(["']?data:[^,]*,?["']?\)/;
|
|
|
44
44
|
const LOCAL_SOURCE = "local(";
|
|
45
45
|
const FONT_MAX_LOAD_DELAY = 5000;
|
|
46
46
|
const DUPLICATE_STYLESHEET_ATTRIBUTE_NAME = "data-sf-duplicate-stylesheet-ref";
|
|
47
|
+
// the attributes that say how the link fetched its stylesheet, which the style element now holding
|
|
48
|
+
// that stylesheet has no use for. Everything else identified the element in the page
|
|
49
|
+
const LINK_FETCH_ATTRIBUTE_NAMES = ["rel", "href", "type", "media", "as", "crossorigin", "integrity",
|
|
50
|
+
"referrerpolicy", "hreflang", "sizes", "imagesrcset", "imagesizes", "fetchpriority"];
|
|
47
51
|
|
|
48
52
|
let util;
|
|
49
53
|
|
|
@@ -162,6 +166,15 @@ function getProcessorHelperClass(utilInstance) {
|
|
|
162
166
|
if (stylesheetInfo) {
|
|
163
167
|
stylesheets.delete(linkElement);
|
|
164
168
|
const styleElement = doc.createElement("style");
|
|
169
|
+
// the element is replaced rather than rewritten, so whatever identified it in
|
|
170
|
+
// the page has to be carried over: an id a script still looks up, a class a
|
|
171
|
+
// selector still matches. The attributes left out are the ones that describe
|
|
172
|
+
// the fetch the style element no longer performs
|
|
173
|
+
Array.from(linkElement.attributes).forEach(({ name, value }) => {
|
|
174
|
+
if (!LINK_FETCH_ATTRIBUTE_NAMES.includes(name.toLowerCase())) {
|
|
175
|
+
styleElement.setAttribute(name, value);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
165
178
|
if (stylesheetInfo.mediaText) {
|
|
166
179
|
styleElement.media = stylesheetInfo.mediaText;
|
|
167
180
|
}
|
|
@@ -36,6 +36,7 @@ const CANVAS_TAG_FOUND = /<canvas/gi;
|
|
|
36
36
|
const EMPTY_URL_SOURCE = /^url\(["']?data:[^,]*,?["']?\)/;
|
|
37
37
|
const LOCAL_SOURCE = "local(";
|
|
38
38
|
const FONT_MAX_LOAD_DELAY = 5000;
|
|
39
|
+
const LINK_OWN_ATTRIBUTE_NAMES = ["rel", "type", "href", "media"];
|
|
39
40
|
|
|
40
41
|
let util;
|
|
41
42
|
|
|
@@ -100,6 +101,7 @@ function getProcessorHelperClass(utilInstance) {
|
|
|
100
101
|
replaceStylesheets(doc, stylesheets, options, resources) {
|
|
101
102
|
const entries = Array.from(stylesheets).reverse();
|
|
102
103
|
const linkElements = new Map();
|
|
104
|
+
const sharedStyleElements = new Map();
|
|
103
105
|
Array.from(new Set(options.inlineStylesheetsRefs.values())).forEach(stylesheetRefIndex => {
|
|
104
106
|
const linkElement = doc.createElement("link");
|
|
105
107
|
linkElement.setAttribute("rel", "stylesheet");
|
|
@@ -117,6 +119,11 @@ function getProcessorHelperClass(utilInstance) {
|
|
|
117
119
|
: cssTree.parse(content, { context: "stylesheet", parseCustomProperty: true });
|
|
118
120
|
resources.stylesheets.set(resources.stylesheets.size, { name, content: this.generateStylesheetContent(stylesheet, options) });
|
|
119
121
|
linkElements.set(stylesheetRefIndex, linkElement);
|
|
122
|
+
// the element the duplicates were folded into is not itself a duplicate, so it is
|
|
123
|
+
// absent from inlineStylesheetsRefs and would keep its content inline: the archive
|
|
124
|
+
// would then hold the same stylesheet twice, once as the entry the links point at
|
|
125
|
+
// and once in the page
|
|
126
|
+
sharedStyleElements.set(styleElement, stylesheetRefIndex);
|
|
120
127
|
});
|
|
121
128
|
for (const [key, stylesheetInfo] of entries) {
|
|
122
129
|
if (key.urlNode) {
|
|
@@ -135,11 +142,22 @@ function getProcessorHelperClass(utilInstance) {
|
|
|
135
142
|
resources.stylesheets.set(resources.stylesheets.size, { name, stylesheet: stylesheetInfo.stylesheet, url: stylesheetInfo.url });
|
|
136
143
|
} else {
|
|
137
144
|
const styleElement = key.element;
|
|
138
|
-
const stylesheetRefIndex = options.inlineStylesheetsRefs.
|
|
145
|
+
const stylesheetRefIndex = options.inlineStylesheetsRefs.has(styleElement)
|
|
146
|
+
? options.inlineStylesheetsRefs.get(styleElement)
|
|
147
|
+
: sharedStyleElements.get(styleElement);
|
|
139
148
|
if (stylesheetRefIndex === undefined) {
|
|
140
149
|
styleElement.textContent = this.generateStylesheetContent(stylesheetInfo.stylesheet, options);
|
|
141
150
|
} else {
|
|
142
151
|
const linkElement = linkElements.get(stylesheetRefIndex).cloneNode(true);
|
|
152
|
+
// the element is replaced rather than rewritten, so whatever identified it in
|
|
153
|
+
// the page has to be carried over: an id a script still looks up, a class a
|
|
154
|
+
// selector still matches. The attributes left out are the ones that describe
|
|
155
|
+
// the link itself, which the code around here sets
|
|
156
|
+
Array.from(styleElement.attributes).forEach(({ name, value }) => {
|
|
157
|
+
if (!LINK_OWN_ATTRIBUTE_NAMES.includes(name.toLowerCase())) {
|
|
158
|
+
linkElement.setAttribute(name, value);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
143
161
|
if (stylesheetInfo.mediaText) {
|
|
144
162
|
linkElement.media = stylesheetInfo.mediaText;
|
|
145
163
|
}
|
|
@@ -92,19 +92,33 @@ function process(doc, stylesheets, styles, options) {
|
|
|
92
92
|
fontsInfo.used = fontsInfo.used.map(fontNames => fontNames.map(familyName => resolveFamilyName(familyName, options)));
|
|
93
93
|
fontsInfo.used = fontsInfo.used.map(fontNames => helper.flatten(fontNames));
|
|
94
94
|
const variableFound = fontsInfo.used.find(fontNames => fontNames.find(fontName => fontName.match(/^var\(--/)));
|
|
95
|
+
// an empty list of rendered fonts does not mean the document uses none: every rendered element
|
|
96
|
+
// has a computed font-family, so an empty list means the computed styles could not be read at
|
|
97
|
+
// all. A frame whose contentDocument is unreachable is re-parsed from its srcdoc with
|
|
98
|
+
// DOMParser, and that document is never rendered, so it reports nothing and every face it
|
|
99
|
+
// declares would be dropped
|
|
100
|
+
const usedFontsUnknown = !options.usedFonts || !options.usedFonts.length;
|
|
95
101
|
let unusedFonts, filteredUsedFonts;
|
|
96
|
-
if (
|
|
102
|
+
if (usedFontsUnknown) {
|
|
97
103
|
unusedFonts = [];
|
|
98
104
|
} else {
|
|
99
105
|
filteredUsedFonts = new Map();
|
|
100
|
-
fontsInfo.used.forEach(fontNames => fontNames.forEach(familyName =>
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
fontsInfo.used.forEach(fontNames => fontNames.forEach(familyName =>
|
|
107
|
+
keepDeclaredFontIfRendered(familyName, fontsInfo, filteredUsedFonts, options)));
|
|
108
|
+
// A family named through a value that could not be resolved cannot be looked up in the
|
|
109
|
+
// stylesheets — but the browser resolved it when it drew the page, so whatever it named is
|
|
110
|
+
// in the list of fonts actually rendered, and that list answers for it.
|
|
111
|
+
//
|
|
112
|
+
// Giving up on the whole document instead, which is what an unresolved name used to do,
|
|
113
|
+
// kept every declared face on any page holding one unreadable value anywhere while every
|
|
114
|
+
// other page pruned as usual. That was not a policy about uncertainty: a page that names
|
|
115
|
+
// its families plainly has always dropped a face it had not drawn yet, and only a page
|
|
116
|
+
// whose value happened not to parse was spared. The difference came from a parse failure,
|
|
117
|
+
// so it is the rendered list that decides in both cases now.
|
|
118
|
+
if (variableFound) {
|
|
119
|
+
fontsInfo.declared.forEach(fontInfo =>
|
|
120
|
+
keepDeclaredFontIfRendered(fontInfo.fontFamily, fontsInfo, filteredUsedFonts, options));
|
|
121
|
+
}
|
|
108
122
|
unusedFonts = fontsInfo.declared.filter(fontInfo => !filteredUsedFonts.has(fontInfo.fontFamily));
|
|
109
123
|
}
|
|
110
124
|
const docChars = Array.from(new Set(docContent)).map(char => char.charCodeAt(0)).sort((value1, value2) => value1 - value2);
|
|
@@ -120,6 +134,17 @@ function process(doc, stylesheets, styles, options) {
|
|
|
120
134
|
return stats;
|
|
121
135
|
}
|
|
122
136
|
|
|
137
|
+
// a face is kept when the page declares it and the browser reports having drawn with it: the
|
|
138
|
+
// stylesheets say what exists, the rendered list says what was needed
|
|
139
|
+
function keepDeclaredFontIfRendered(familyName, fontsInfo, filteredUsedFonts, options) {
|
|
140
|
+
if (fontsInfo.declared.find(fontInfo => fontInfo.fontFamily == familyName)) {
|
|
141
|
+
const optionalData = options.usedFonts.filter(fontInfo => fontInfo[0] == familyName);
|
|
142
|
+
if (optionalData.length) {
|
|
143
|
+
filteredUsedFonts.set(familyName, optionalData);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
123
148
|
function getFontsInfo(cssRules, fontsInfo, options) {
|
|
124
149
|
cssRules.forEach(ruleData => {
|
|
125
150
|
if (ruleData.type == "Atrule" && (ruleData.name == "media" || ruleData.name == "supports" || ruleData.name == "layer" || ruleData.name == "container") && ruleData.block && ruleData.block.children) {
|
|
@@ -200,25 +225,68 @@ function getCustomPropertyValues(name, options) {
|
|
|
200
225
|
return values;
|
|
201
226
|
}
|
|
202
227
|
|
|
203
|
-
function resolveFamilyName(familyName, options) {
|
|
228
|
+
function resolveFamilyName(familyName, options, resolvedProperties = new Set()) {
|
|
204
229
|
const matchedVar = familyName.match(REGEXP_CUSTOM_PROPERTY_FAMILY);
|
|
205
230
|
if (matchedVar) {
|
|
231
|
+
const propertyName = matchedVar[1];
|
|
206
232
|
const fallback = matchedVar[2];
|
|
207
|
-
// a
|
|
208
|
-
//
|
|
209
|
-
if (!
|
|
210
|
-
const
|
|
233
|
+
// a property naming itself, directly or through another one, would resolve for ever: the
|
|
234
|
+
// chain already walked is carried down the branch so it stops instead
|
|
235
|
+
if (!resolvedProperties.has(propertyName)) {
|
|
236
|
+
const properties = new Set(resolvedProperties);
|
|
237
|
+
properties.add(propertyName);
|
|
238
|
+
const values = getCustomPropertyValues(propertyName, options);
|
|
211
239
|
if (values) {
|
|
212
|
-
const families = helper.flatten(values.map(value => splitFamilyNames(value)));
|
|
213
|
-
|
|
240
|
+
const families = helper.flatten(values.map(value => splitFamilyNames(value, options, properties)));
|
|
241
|
+
const fallbackFamilies = fallback ? splitFamilyNames(fallback, options, properties) : [];
|
|
242
|
+
// the browser takes the property or the fallback, so knowing one branch is not
|
|
243
|
+
// knowing the value: a var() left unresolved in either one keeps the family
|
|
244
|
+
// undetermined, exactly as it was before the nested one could be read at all
|
|
245
|
+
if (!families.concat(fallbackFamilies).some(testUnresolvedFamilyName)) {
|
|
246
|
+
return families.concat(fallbackFamilies);
|
|
247
|
+
}
|
|
214
248
|
}
|
|
215
249
|
}
|
|
216
250
|
}
|
|
217
251
|
return familyName;
|
|
218
252
|
}
|
|
219
253
|
|
|
220
|
-
function
|
|
221
|
-
return
|
|
254
|
+
function testUnresolvedFamilyName(familyName) {
|
|
255
|
+
return typeof familyName == "string" && familyName.startsWith("var(");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function splitFamilyNames(value, options, resolvedProperties) {
|
|
259
|
+
const familyNames = splitValues(value).map(familyName => normalizeFamilyName(familyName)).filter(familyName => familyName);
|
|
260
|
+
return options
|
|
261
|
+
? helper.flatten(familyNames.map(familyName => resolveFamilyName(familyName, options, resolvedProperties)))
|
|
262
|
+
: familyNames;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// a family list is split on its top-level commas only: the commas inside a var() belong to that
|
|
266
|
+
// var(), and the ones inside a quoted name belong to the name. Splitting on every comma is what
|
|
267
|
+
// made a var() nested in a fallback unreadable, and it also broke a family named "Foo, Bar"
|
|
268
|
+
function splitValues(value) {
|
|
269
|
+
const values = [];
|
|
270
|
+
let depth = 0, quote, start = 0;
|
|
271
|
+
for (let index = 0; index < value.length; index++) {
|
|
272
|
+
const character = value.charAt(index);
|
|
273
|
+
if (quote) {
|
|
274
|
+
if (character == quote && value.charAt(index - 1) != "\\") {
|
|
275
|
+
quote = null;
|
|
276
|
+
}
|
|
277
|
+
} else if (character == "\"" || character == "'") {
|
|
278
|
+
quote = character;
|
|
279
|
+
} else if (character == "(") {
|
|
280
|
+
depth++;
|
|
281
|
+
} else if (character == ")") {
|
|
282
|
+
depth--;
|
|
283
|
+
} else if (character == "," && !depth) {
|
|
284
|
+
values.push(value.substring(start, index));
|
|
285
|
+
start = index + 1;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
values.push(value.substring(start));
|
|
289
|
+
return values;
|
|
222
290
|
}
|
|
223
291
|
|
|
224
292
|
function filterUnusedFonts(cssRules, declaredFonts, unusedFonts, filteredUsedFonts, docChars) {
|
package/package.json
CHANGED
|
@@ -31,7 +31,8 @@ import {
|
|
|
31
31
|
ZipReader
|
|
32
32
|
} from "./../../vendor/zip/zip.js";
|
|
33
33
|
import {
|
|
34
|
-
createArchive
|
|
34
|
+
createArchive,
|
|
35
|
+
escapeHTML
|
|
35
36
|
} from "./compression.js";
|
|
36
37
|
|
|
37
38
|
const browser = globalThis.browser;
|
|
@@ -232,13 +233,3 @@ function getTOCContent(pages) {
|
|
|
232
233
|
pages.map(page => "<li><a href=\"" + escapeHTML(page.url) + "\">" + escapeHTML(page.title || page.url) + "</a></li>").join("") +
|
|
233
234
|
"</ul></nav>";
|
|
234
235
|
}
|
|
235
|
-
|
|
236
|
-
// the prelude declares the windows-1252 charset, non-ASCII characters must be
|
|
237
|
-
// encoded as HTML entities to survive it
|
|
238
|
-
function escapeHTML(value) {
|
|
239
|
-
return Array.from(value).map(character => {
|
|
240
|
-
const codePoint = character.codePointAt(0);
|
|
241
|
-
return codePoint < 32 || codePoint > 126 || character == "&" || character == "<" || character == ">" || character == "\"" ?
|
|
242
|
-
"&#" + codePoint + ";" : character;
|
|
243
|
-
}).join("");
|
|
244
|
-
}
|
|
@@ -151,15 +151,27 @@ const PROCESS_OPTION_NAMES = [
|
|
|
151
151
|
export {
|
|
152
152
|
process,
|
|
153
153
|
createArchive,
|
|
154
|
+
escapeHTML,
|
|
154
155
|
PROCESS_OPTION_NAMES
|
|
155
156
|
};
|
|
156
157
|
|
|
157
158
|
async function process(pageData, options, lastModDate = new Date()) {
|
|
158
159
|
let script;
|
|
160
|
+
// The worker is configured before anything else, and outside the extension it is turned off
|
|
161
|
+
// rather than left alone. Given no address, zip.js resolves its default one against the page
|
|
162
|
+
// being saved, so the browser asks the CAPTURED SITE for a file that site has never heard of:
|
|
163
|
+
// three 404s in the user's own server logs for every archive, and then a fallback to the main
|
|
164
|
+
// thread anyway, which is where the work was always going to happen. Choosing the fallback
|
|
165
|
+
// costs nothing that was ever gained and asks the site for nothing.
|
|
166
|
+
const extensionContext = Boolean(browser && browser.runtime && browser.runtime.getURL);
|
|
167
|
+
if (extensionContext) {
|
|
168
|
+
configure({ workerURI: "/lib/single-file-z-worker.js" });
|
|
169
|
+
} else {
|
|
170
|
+
configure({ useWebWorkers: false });
|
|
171
|
+
}
|
|
159
172
|
if (options.zipScript) {
|
|
160
173
|
script = options.zipScript;
|
|
161
|
-
} else if (
|
|
162
|
-
configure({ workerURI: "/lib/single-file-z-worker.js" });
|
|
174
|
+
} else if (extensionContext) {
|
|
163
175
|
script = await (await fetch(browser.runtime.getURL(SCRIPT_PATH))).text();
|
|
164
176
|
}
|
|
165
177
|
return createArchive(pageData, options, script, zipWriter => {
|
|
@@ -622,7 +634,7 @@ function getHTMLHeadData(pageData, options) {
|
|
|
622
634
|
let pageContent = "";
|
|
623
635
|
// the title is left out of a password-protected archive: manifest.json carries it too and
|
|
624
636
|
// is encrypted, so emitting it here would publish what the password is meant to cover
|
|
625
|
-
const title = options.password ? "" :
|
|
637
|
+
const title = options.password ? "" : escapeHTML(pageData.title || "");
|
|
626
638
|
pageContent += "<title>" + title + "</title>";
|
|
627
639
|
// the canonical link publishes the URL the archive was saved from, for the same reason
|
|
628
640
|
// the title above is left out of a password-protected archive
|
|
@@ -644,12 +656,13 @@ function getHTMLHeadData(pageData, options) {
|
|
|
644
656
|
return pageContent;
|
|
645
657
|
}
|
|
646
658
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
659
|
+
// every piece of text in the prelude goes through this, wherever it is assembled: the prelude
|
|
660
|
+
// declares a single-byte charset, and numeric character references are ASCII bytes, so they
|
|
661
|
+
// survive it and any parser decodes them back to the original text
|
|
662
|
+
function escapeHTML(value) {
|
|
663
|
+
return Array.from(value).map(character => {
|
|
651
664
|
const codePoint = character.codePointAt(0);
|
|
652
|
-
return codePoint < 32 || codePoint > 126 || character == "&" || character == "<" || character == ">" ?
|
|
665
|
+
return codePoint < 32 || codePoint > 126 || character == "&" || character == "<" || character == ">" || character == "\"" ?
|
|
653
666
|
"&#" + codePoint + ";" : character;
|
|
654
667
|
}).join("");
|
|
655
668
|
}
|
|
@@ -11,6 +11,13 @@
|
|
|
11
11
|
// so it can only keep too much, never too little — and options.usedFonts, which comes from the
|
|
12
12
|
// rendered computed styles, gates the result anyway.
|
|
13
13
|
//
|
|
14
|
+
// What happens when even the union cannot answer changed with it. Switching pruning off for the
|
|
15
|
+
// whole document was never a policy about uncertainty — a page naming its families plainly has
|
|
16
|
+
// always dropped a face it had not drawn yet, and only a page holding one unreadable value anywhere
|
|
17
|
+
// was spared, all of it. So the rendered list decides in that case too: a family the browser
|
|
18
|
+
// resolved when it drew the page is in that list whatever the stylesheets can be made to say. The
|
|
19
|
+
// one case that still keeps everything is the one where the rendering itself is missing.
|
|
20
|
+
//
|
|
14
21
|
// The other var() defects this pins, all found alongside it:
|
|
15
22
|
// - custom property names are case-sensitive; they were lowercased with the family names, so
|
|
16
23
|
// var(--ProbeFont) never resolved, not even from :root
|
|
@@ -45,6 +52,11 @@ const USED_FONTS = [
|
|
|
45
52
|
|
|
46
53
|
const ALL_FAMILIES = ["usedone", "usedtwo", "usedthree", "unused"];
|
|
47
54
|
|
|
55
|
+
// what survives when the stylesheets cannot say which family a value names: the faces the page
|
|
56
|
+
// declares AND the rendering reports having drawn with. It is USED_FONTS that decides there, so a
|
|
57
|
+
// face nothing drew is dropped even though nothing could be shown to name it either
|
|
58
|
+
const RENDERED_FAMILIES = ["usedone", "usedtwo", "usedthree"];
|
|
59
|
+
|
|
48
60
|
let failures = 0;
|
|
49
61
|
|
|
50
62
|
check("a plain family name prunes the rest",
|
|
@@ -83,12 +95,18 @@ check("the font shorthand resolves a property written with a fallback",
|
|
|
83
95
|
run({ rules: ".card{--probe-font:\"UsedOne\"}.card p{font:italic 1em var(--probe-font,serif)}" }),
|
|
84
96
|
["usedone"]);
|
|
85
97
|
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
98
|
+
// The other half of the contract: what happens when the value genuinely cannot be determined. The
|
|
99
|
+
// stylesheets no longer decide there — the rendering does. The browser resolved the value when it
|
|
100
|
+
// drew the page, so whatever the property named is in the list of fonts reported as used, and a
|
|
101
|
+
// face absent from that list was drawn by nothing.
|
|
102
|
+
//
|
|
103
|
+
// It used to keep EVERY declared face, and not as a decision about uncertainty: a page naming its
|
|
104
|
+
// families plainly has always dropped a face it had not drawn yet, and only a page holding one
|
|
105
|
+
// value that happened not to parse was spared — the whole document, over one unreadable name. The
|
|
106
|
+
// case these checks still guard is the one below them, where the rendering itself is unavailable.
|
|
107
|
+
check("a property declared nowhere falls back to the fonts that were drawn",
|
|
90
108
|
run({ rules: ".card p{font-family:var(--set-by-script),serif}" }),
|
|
91
|
-
|
|
109
|
+
RENDERED_FAMILIES);
|
|
92
110
|
|
|
93
111
|
// the shorthand cannot be substituted with several candidate values, but its var() sits in family
|
|
94
112
|
// position, so the parser hands it back as the family and the union answers it there instead
|
|
@@ -97,18 +115,51 @@ check("a font shorthand with several candidates resolves through the family",
|
|
|
97
115
|
["usedone", "usedtwo"]);
|
|
98
116
|
|
|
99
117
|
// a property holding the whole shorthand is the case the union must NOT touch: its values are not
|
|
100
|
-
// family lists, and reading them as such would
|
|
101
|
-
check("a property holding a whole shorthand
|
|
118
|
+
// family lists, and reading them as such would name families the document never had
|
|
119
|
+
check("a property holding a whole shorthand falls back to the fonts that were drawn",
|
|
102
120
|
run({ rules: ".card{--font:italic 1em \"UsedOne\"}.note{--font:italic 1em \"UsedTwo\"}p{font:var(--font)}" }),
|
|
103
|
-
|
|
121
|
+
RENDERED_FAMILIES);
|
|
104
122
|
|
|
105
|
-
check("a var() nested in a fallback
|
|
123
|
+
check("a var() nested in a fallback falls back to the fonts that were drawn",
|
|
106
124
|
run({ rules: ".card{--probe-font:\"UsedOne\"}.card p{font-family:var(--other,var(--probe-font),serif)}" }),
|
|
125
|
+
RENDERED_FAMILIES);
|
|
126
|
+
|
|
127
|
+
// the check above leaves the family undetermined because the OUTER property is declared nowhere.
|
|
128
|
+
// A var() written in the font-family itself is split by the AST walk, which hands each branch over
|
|
129
|
+
// separately, so nesting alone was never the problem there. The chain that did give up is a var()
|
|
130
|
+
// inside a property VALUE: it was read one level deep and whatever it named stayed unresolved,
|
|
131
|
+
// which switched pruning off for the whole document
|
|
132
|
+
check("a property whose value is another declared property resolves through it",
|
|
133
|
+
run({ rules: ".card{--first-font:var(--second-font)}.note{--second-font:\"UsedOne\"}.card p{font-family:var(--first-font),serif}" }),
|
|
134
|
+
["usedone"]);
|
|
135
|
+
|
|
136
|
+
// splitting that value on every comma cut this one into "var(--second-font" and "\"UsedTwo\")",
|
|
137
|
+
// two names that resolve to nothing, and the document went undetermined over a value it holds in full
|
|
138
|
+
check("a value holding a var() with its own fallback is split on the top-level comma",
|
|
139
|
+
run({ rules: ".card{--first-font:var(--second-font,\"UsedTwo\"),serif}.note{--second-font:\"UsedOne\"}.card p{font-family:var(--first-font)}" }),
|
|
140
|
+
["usedone", "usedtwo"]);
|
|
141
|
+
|
|
142
|
+
// two properties naming each other resolve for ever without the guard: this check hangs rather
|
|
143
|
+
// than fails when it regresses
|
|
144
|
+
check("a property naming itself through another one falls back to the fonts that were drawn",
|
|
145
|
+
run({ rules: ".card{--first-font:var(--second-font)}.note{--second-font:var(--first-font)}.card p{font-family:var(--first-font),serif}" }),
|
|
146
|
+
RENDERED_FAMILIES);
|
|
147
|
+
|
|
148
|
+
// The rendered-fonts list is what says a declared face is really drawn, and an EMPTY one is not
|
|
149
|
+
// the same answer as a short one: every rendered element has a computed font-family, so an empty
|
|
150
|
+
// list means the computed styles could not be read at all. It happens for real. A frame whose
|
|
151
|
+
// contentDocument is unreachable is re-parsed from its srcdoc with DOMParser
|
|
152
|
+
// (processors/frame-tree/content/content-frame-tree.js), and that document is never rendered, so
|
|
153
|
+
// it reports no font and the frame lost EVERY face it declared — measured on derstandard.at,
|
|
154
|
+
// where the newsletter box inside such a frame fell back to a system font, and on MDN, where the
|
|
155
|
+
// text in the CSS-demo frame reflowed. The families are named right there in the frame's own CSS.
|
|
156
|
+
check("a document that reports no rendered font keeps every font",
|
|
157
|
+
run({ rules: ".card p{font-family:\"UsedOne\",serif}", usedFonts: [] }),
|
|
107
158
|
ALL_FAMILIES);
|
|
108
159
|
|
|
109
|
-
check("a property whose value is another undetermined property
|
|
160
|
+
check("a property whose value is another undetermined property falls back to the fonts that were drawn",
|
|
110
161
|
run({ rules: ".card{--probe-font:var(--set-by-script)}.card p{font-family:var(--probe-font),serif}" }),
|
|
111
|
-
|
|
162
|
+
RENDERED_FAMILIES);
|
|
112
163
|
|
|
113
164
|
// An unquoted family name is a sequence of identifier tokens, and the walk that joins them has to
|
|
114
165
|
// resume after the LAST of them. Resuming after the first pushed every word but that one again as a
|
|
@@ -2,7 +2,9 @@ import "./dom-stub.js";
|
|
|
2
2
|
import { makePageData, makeOptions, runProcess, mulberry32 } from "./common.js";
|
|
3
3
|
import { ZipReader, BlobReader } from "../../vendor/zip/zip.js";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// the quote is there because the escaper the title shares with the table of contents encodes
|
|
6
|
+
// it for an attribute value, where it matters, and a title has to round-trip through that too
|
|
7
|
+
const TITLE = "日本語 — café & <b> \"quoted\"";
|
|
6
8
|
const PDF = new TextEncoder().encode("%PDF-1.4\n1 0 obj\n<<>>\nendobj\ntrailer\n<<>>\n%%EOF\n");
|
|
7
9
|
|
|
8
10
|
let failed = false;
|