single-file-core 1.6.1 → 1.6.3

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.
Files changed (79) hide show
  1. package/core/filename.js +62 -0
  2. package/core/helper.js +9 -32
  3. package/modules/css-fonts-minifier.js +34 -13
  4. package/modules/template-formatter.js +8 -2
  5. package/package.json +10 -2
  6. package/processors/compression/compression-constants.js +28 -0
  7. package/processors/compression/compression.js +3 -1
  8. package/.github/workflows/publish.yml +0 -88
  9. package/.github/workflows/test.yml +0 -31
  10. package/.github/workflows/vendor.yml +0 -39
  11. package/css-tree-build/README.md +0 -39
  12. package/css-tree-build/build.js +0 -66
  13. package/css-tree-build/package-lock.json +0 -511
  14. package/css-tree-build/package.json +0 -13
  15. package/deno.lock +0 -19
  16. package/doc/assets/singlefile-archive-byte-map.svg +0 -276
  17. package/doc/assets/singlefile-archive-lenses.svg +0 -245
  18. package/doc/singlefile-archive.md +0 -1968
  19. package/eslint.config.mjs +0 -74
  20. package/test/capture/README.md +0 -74
  21. package/test/capture/canonical-link.js +0 -50
  22. package/test/capture/common.js +0 -73
  23. package/test/capture/deferred-content-options.js +0 -71
  24. package/test/capture/dom.js +0 -26
  25. package/test/capture/maff-metadata.js +0 -205
  26. package/test/capture/resource-cap.js +0 -79
  27. package/test/capture/script-uri-sanitization.js +0 -82
  28. package/test/fixtures/README.md +0 -18
  29. package/test/fixtures/classic-digest.html +0 -5
  30. package/test/fixtures/generate-fixtures.sh +0 -31
  31. package/test/fixtures/multi-page-dedup.zip.html +0 -70
  32. package/test/fixtures/multi-page.zip.html +0 -26
  33. package/test/fixtures/single-page.zip.html +0 -11
  34. package/test/fixtures/site/multi-page/alpha.html +0 -2
  35. package/test/fixtures/site/multi-page/beta.html +0 -2
  36. package/test/fixtures/site/multi-page/index.html +0 -2
  37. package/test/fixtures/site/multi-page/sub/gamma.html +0 -2
  38. package/test/fixtures/site/multi-page-dedup/index.html +0 -2
  39. package/test/fixtures/site/multi-page-dedup/logo.png +0 -0
  40. package/test/fixtures/site/multi-page-dedup/one.html +0 -2
  41. package/test/fixtures/site/multi-page-dedup/shared.css +0 -3
  42. package/test/fixtures/site/multi-page-dedup/two.html +0 -2
  43. package/test/run.js +0 -109
  44. package/test/sfz-harness/README.md +0 -87
  45. package/test/sfz-harness/adopted-stylesheets-hook.js +0 -240
  46. package/test/sfz-harness/byte-map.js +0 -137
  47. package/test/sfz-harness/charset-round-trip.js +0 -161
  48. package/test/sfz-harness/check-determinism.js +0 -57
  49. package/test/sfz-harness/common.js +0 -154
  50. package/test/sfz-harness/content-type-sniffing.js +0 -83
  51. package/test/sfz-harness/css-fonts-minifier.js +0 -235
  52. package/test/sfz-harness/css-property-filter.js +0 -113
  53. package/test/sfz-harness/dom-stub.js +0 -13
  54. package/test/sfz-harness/entry-compression.js +0 -77
  55. package/test/sfz-harness/filename-characters.js +0 -55
  56. package/test/sfz-harness/filename-max-length.js +0 -69
  57. package/test/sfz-harness/font-face-composite.js +0 -135
  58. package/test/sfz-harness/format-rules.js +0 -617
  59. package/test/sfz-harness/gen-e2e-page.js +0 -14
  60. package/test/sfz-harness/inlined-functions.js +0 -82
  61. package/test/sfz-harness/option-wiring.js +0 -60
  62. package/test/sfz-harness/pages-archive.js +0 -318
  63. package/test/sfz-harness/pages-router.js +0 -143
  64. package/test/sfz-harness/relocation-cost.js +0 -94
  65. package/test/sfz-harness/search-triggers.js +0 -35
  66. package/test/sfz-harness/smoke.js +0 -10
  67. package/test/sfz-harness/stored-trigger.js +0 -95
  68. package/test/sfz-harness/trigger-seeds.json +0 -120
  69. package/test/sfz-harness/zip64.js +0 -77
  70. package/zip-build/README.md +0 -56
  71. package/zip-build/lib/fflate-streams.js +0 -47
  72. package/zip-build/lib/fflate.js +0 -2692
  73. package/zip-build/lib/zip-vendor-worker.js +0 -3
  74. package/zip-build/lib/zip-vendor.js +0 -9
  75. package/zip-build/lib/zip.js +0 -25
  76. package/zip-build/package-lock.json +0 -665
  77. package/zip-build/package.json +0 -15
  78. package/zip-build/reserved-property-names.json +0 -10630
  79. package/zip-build/rollup.config.js +0 -61
@@ -0,0 +1,62 @@
1
+ /*
2
+ * Copyright 2010-2022 Gildas Lormeau
3
+ * contact : gildas.lormeau <at> gmail.com
4
+ *
5
+ * This file is part of SingleFile.
6
+ *
7
+ * The code in this file is free software: you can redistribute it and/or
8
+ * modify it under the terms of the GNU Affero General Public License
9
+ * (GNU AGPL) as published by the Free Software Foundation, either version 3
10
+ * of the License, or (at your option) any later version.
11
+ *
12
+ * The code in this file is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
15
+ * General Public License for more details.
16
+ *
17
+ * As additional permission under GNU AGPL version 3 section 7, you may
18
+ * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
19
+ * AGPL normally required by section 4, provided you include this license
20
+ * notice and a URL through which recipients can access the Corresponding
21
+ * Source.
22
+ */
23
+
24
+ const DEFAULT_REPLACED_CHARACTERS = ["~", "+", "?", "%", "*", ":", "|", "\"", "<", ">", "\\\\", "\x00-\x1f", "\x7F"];
25
+ const DEFAULT_REPLACEMENT_CHARACTER = "_";
26
+ const DEFAULT_REPLACEMENT_CHARACTERS = ["~", "+", "?", "%", "*", ":", "|", """, "<", ">", "\"];
27
+ const CHARACTER_CLASS_SPECIAL_CHARACTERS = ["[", "]", "^", "-", "\\"];
28
+
29
+ export {
30
+ getValidFilename,
31
+ DEFAULT_REPLACED_CHARACTERS,
32
+ DEFAULT_REPLACEMENT_CHARACTER,
33
+ DEFAULT_REPLACEMENT_CHARACTERS
34
+ };
35
+
36
+ function getValidFilename(filename, replacedCharacters = DEFAULT_REPLACED_CHARACTERS, replacementCharacter = DEFAULT_REPLACEMENT_CHARACTER, replacementCharacters = DEFAULT_REPLACEMENT_CHARACTERS) {
37
+ replacementCharacters.forEach((indexReplacementCharacter, index) => {
38
+ if (indexReplacementCharacter && replacedCharacters[index] !== undefined && indexReplacementCharacter != replacedCharacters[index]) {
39
+ // no "+" here, unlike the fallback below: a lookalike replaces its character one for
40
+ // one, so collapsing a run would drop characters the name needs ("C++" -> "C+")
41
+ filename = filename.replace(new RegExp("[" + getCharacterClassContent(replacedCharacters[index]) + "]", "g"), indexReplacementCharacter);
42
+ }
43
+ });
44
+ replacedCharacters.forEach((replacedCharacter, index) => {
45
+ if (!replacementCharacters[index]) {
46
+ filename = filename.replace(new RegExp("[" + getCharacterClassContent(replacedCharacter) + "]+", "g"), replacementCharacter);
47
+ }
48
+ });
49
+ filename = filename
50
+ .replace(/\.\.\//g, "")
51
+ .replace(/^\/+/, "")
52
+ .replace(/\/+/g, "/")
53
+ .replace(/\/$/, "")
54
+ .replace(/\.$/, "")
55
+ .replace(/\.\//g, "." + replacementCharacter)
56
+ .replace(/\/\./g, "/" + replacementCharacter);
57
+ return filename;
58
+ }
59
+
60
+ function getCharacterClassContent(characters) {
61
+ return characters.length == 1 && CHARACTER_CLASS_SPECIAL_CHARACTERS.includes(characters) ? "\\" + characters : characters;
62
+ }
package/core/helper.js CHANGED
@@ -32,6 +32,12 @@ import {
32
32
  MESSAGE_PREFIX,
33
33
  NO_SCRIPT_PROPERTY_NAME
34
34
  } from "./constants.js";
35
+ import {
36
+ getValidFilename,
37
+ DEFAULT_REPLACED_CHARACTERS,
38
+ DEFAULT_REPLACEMENT_CHARACTER,
39
+ DEFAULT_REPLACEMENT_CHARACTERS
40
+ } from "./filename.js";
35
41
 
36
42
  const ON_BEFORE_CAPTURE_EVENT_NAME = SINGLE_FILE_PREFIX + "on-before-capture";
37
43
  const ON_AFTER_CAPTURE_EVENT_NAME = SINGLE_FILE_PREFIX + "on-after-capture";
@@ -77,10 +83,6 @@ const INFOBAR_TAGNAME = infobar.INFOBAR_TAGNAME;
77
83
  const EMPTY_RESOURCE = "data:,";
78
84
  const POSTER_CONTENT_TYPES = ["image/webp", "image/jpeg"];
79
85
  const POSTER_QUALITY = 0.8;
80
- const DEFAULT_REPLACED_CHARACTERS = ["~", "+", "?", "%", "*", ":", "|", "\"", "<", ">", "\\\\", "\x00-\x1f", "\x7F"];
81
- const DEFAULT_REPLACEMENT_CHARACTER = "_";
82
- const DEFAULT_REPLACEMENT_CHARACTERS = ["~", "+", "?", "%", "*", ":", "|", """, "<", ">", "\"];
83
- const CHARACTER_CLASS_SPECIAL_CHARACTERS = ["[", "]", "^", "-", "\\"];
84
86
  const NESTING_TRACK_ID_ATTRIBUTE_NAME = "data-sf-nesting-track-id";
85
87
  const DEPRECATED_OPTION_NAMES = {
86
88
  loadDeferredImages: "loadDeferredContent",
@@ -123,6 +125,9 @@ export {
123
125
  getDataURI,
124
126
  digest,
125
127
  getValidFilename,
128
+ DEFAULT_REPLACED_CHARACTERS,
129
+ DEFAULT_REPLACEMENT_CHARACTER,
130
+ DEFAULT_REPLACEMENT_CHARACTERS,
126
131
  parseDocContent,
127
132
  markInvalidNesting,
128
133
  fixInvalidNesting,
@@ -924,34 +929,6 @@ function getComputedStyle(win, element, pseudoElement) {
924
929
  }
925
930
  }
926
931
 
927
- function getValidFilename(filename, replacedCharacters = DEFAULT_REPLACED_CHARACTERS, replacementCharacter = DEFAULT_REPLACEMENT_CHARACTER, replacementCharacters = DEFAULT_REPLACEMENT_CHARACTERS) {
928
- replacementCharacters.forEach((indexReplacementCharacter, index) => {
929
- if (replacedCharacters[index] !== undefined && indexReplacementCharacter != replacedCharacters[index]) {
930
- // no "+" here, unlike the fallback below: a lookalike replaces its character one for
931
- // one, so collapsing a run would drop characters the name needs ("C++" -> "C+")
932
- filename = filename.replace(new RegExp("[" + getCharacterClassContent(replacedCharacters[index]) + "]", "g"), indexReplacementCharacter);
933
- }
934
- });
935
- replacedCharacters.forEach((replacedCharacter, index) => {
936
- if (replacementCharacters[index] === undefined) {
937
- filename = filename.replace(new RegExp("[" + getCharacterClassContent(replacedCharacter) + "]+", "g"), replacementCharacter);
938
- }
939
- });
940
- filename = filename
941
- .replace(/\.\.\//g, "")
942
- .replace(/^\/+/, "")
943
- .replace(/\/+/g, "/")
944
- .replace(/\/$/, "")
945
- .replace(/\.$/, "")
946
- .replace(/\.\//g, "." + replacementCharacter)
947
- .replace(/\/\./g, "/" + replacementCharacter);
948
- return filename;
949
- }
950
-
951
- function getCharacterClassContent(characters) {
952
- return characters.length == 1 && CHARACTER_CLASS_SPECIAL_CHARACTERS.includes(characters) ? "\\" + characters : characters;
953
- }
954
-
955
932
  function parseDocContent(content, baseURI) {
956
933
  const doc = (new DOMParser()).parseFromString(content, "text/html");
957
934
  if (!doc.head) {
@@ -38,6 +38,9 @@ const helper = {
38
38
  };
39
39
 
40
40
  const REGEXP_COMMA = /\s*,\s*/;
41
+ const REGEXP_SPACES = /\s+/g;
42
+ const REGEXP_FONT_STYLE_ANGLE = /-?\d*\.?\d+/g;
43
+ const DEFAULT_OBLIQUE_ANGLE = 14;
41
44
  const REGEXP_DASH = /-/;
42
45
  const REGEXP_QUESTION_MARK = /\?/g;
43
46
  const REGEXP_STARTS_U_PLUS = /^U\+/i;
@@ -323,11 +326,11 @@ function testUsedFont(ruleData, familyName, declaredFonts, filteredUsedFonts) {
323
326
  let test;
324
327
  const optionalUsedFonts = filteredUsedFonts && filteredUsedFonts.get(familyName);
325
328
  if (optionalUsedFonts && optionalUsedFonts.length) {
326
- let fontStyle = getDeclarationValue(ruleData.block.children, "font-style") || "normal";
329
+ const fontStyle = getDeclarationValue(ruleData.block.children, "font-style") || "normal";
327
330
  if (VALID_FONT_STYLES.find(rule => fontStyle.trim().match(rule))) {
328
331
  const fontWeight = helper.getFontWeight(getDeclarationValue(ruleData.block.children, "font-weight") || "400");
329
332
  const declaredFontsWeights = declaredFonts
330
- .filter(fontInfo => fontInfo.fontFamily == familyName && fontInfo.fontStyle == fontStyle)
333
+ .filter(fontInfo => fontInfo.fontFamily == familyName && testFontStyle(fontInfo.fontStyle, fontStyle))
331
334
  .map(fontInfo => fontInfo.fontWeight.split(" "))
332
335
  .sort((weight1, weight2) => Number.parseInt(weight1[0], 10) - Number.parseInt(weight2[0], 10));
333
336
  let usedFontWeights = optionalUsedFonts
@@ -343,16 +346,6 @@ function testUsedFont(ruleData, familyName, declaredFonts, filteredUsedFonts) {
343
346
  })
344
347
  .filter(fontWeight => fontWeight);
345
348
  test = testFontweight(fontWeight, usedFontWeights);
346
- if (!test) {
347
- usedFontWeights = optionalUsedFonts
348
- .map(fontInfo => {
349
- fontInfo = Array.from(fontInfo);
350
- fontInfo[2] = fontStyle = "normal";
351
- return getUsedFontWeight(fontInfo, fontStyle, declaredFontsWeights);
352
- })
353
- .filter(fontWeight => fontWeight);
354
- test = testFontweight(fontWeight, usedFontWeights);
355
- }
356
349
  }
357
350
  } else {
358
351
  test = true;
@@ -363,6 +356,34 @@ function testUsedFont(ruleData, familyName, declaredFonts, filteredUsedFonts) {
363
356
  return test;
364
357
  }
365
358
 
359
+ function testFontStyle(fontStyle, otherFontStyle) {
360
+ const angles = getFontStyleAngles(fontStyle);
361
+ const otherAngles = getFontStyleAngles(otherFontStyle);
362
+ if (angles && otherAngles) {
363
+ return angles[0] <= otherAngles[1] && otherAngles[0] <= angles[1];
364
+ } else {
365
+ return normalizeFontStyle(fontStyle) == normalizeFontStyle(otherFontStyle);
366
+ }
367
+ }
368
+
369
+ function getFontStyleAngles(fontStyle) {
370
+ fontStyle = normalizeFontStyle(fontStyle);
371
+ if (fontStyle == "normal") {
372
+ return [0, 0];
373
+ } else if (fontStyle == "italic") {
374
+ return [DEFAULT_OBLIQUE_ANGLE, DEFAULT_OBLIQUE_ANGLE];
375
+ } else if (fontStyle == "oblique" || fontStyle.startsWith("oblique ")) {
376
+ const angles = (fontStyle.match(REGEXP_FONT_STYLE_ANGLE) || [])
377
+ .map(angle => Number.parseFloat(angle))
378
+ .filter(angle => !Number.isNaN(angle));
379
+ return angles.length ? [Math.min(...angles), Math.max(...angles)] : [DEFAULT_OBLIQUE_ANGLE, DEFAULT_OBLIQUE_ANGLE];
380
+ }
381
+ }
382
+
383
+ function normalizeFontStyle(fontStyle) {
384
+ return String(fontStyle || "normal").trim().toLowerCase().replace(REGEXP_SPACES, " ");
385
+ }
386
+
366
387
  function testFontweight(fontWeight, usedFontWeights) {
367
388
  let test;
368
389
  for (const fontWeightValue of fontWeight.split(",")) {
@@ -503,7 +524,7 @@ function parseFamilyNames(fontFamilyNameTokenData, fontFamilyNames) {
503
524
  function getUsedFontWeight(fontInfo, fontStyle, fontWeights) {
504
525
  let foundWeight;
505
526
  fontWeights = fontWeights.map(weights => weights.map(value => String(Number.parseInt(value, 10))));
506
- if (fontInfo[2] == fontStyle) {
527
+ if (testFontStyle(fontInfo[2], fontStyle)) {
507
528
  let fontWeight = Number(fontInfo[1]);
508
529
  if (fontWeights.length > 1) {
509
530
  if (fontWeight >= 400 && fontWeight <= 500) {
@@ -16881,8 +16881,14 @@ async function formatFilename(content, doc, options) {
16881
16881
  filename = filename.replace(/\//g, filenameReplacementCharacter);
16882
16882
  }
16883
16883
  if (!options.keepFilename && ((options.filenameMaxLengthUnit == "bytes" && getContentSize(filename) > options.filenameMaxLength) || filename.length > options.filenameMaxLength)) {
16884
- const extensionMatch = filename.match(/(\.[^.]{3,4})$/);
16885
- const extension = extensionMatch && extensionMatch[0] && extensionMatch[0].length > 1 ? extensionMatch[0] : "";
16884
+ const filenameExtension = "." + getFilenameExtension(options);
16885
+ let extension;
16886
+ if (filename.endsWith(filenameExtension)) {
16887
+ extension = filenameExtension;
16888
+ } else {
16889
+ const extensionMatch = filename.match(/(\.[^.]{3,4})$/);
16890
+ extension = extensionMatch && extensionMatch[0] && extensionMatch[0].length > 1 ? extensionMatch[0] : "";
16891
+ }
16886
16892
  const suffix = "…" + extension;
16887
16893
  const maxLength = Math.max(options.filenameMaxLength - (options.filenameMaxLengthUnit == "bytes" ? getContentSize(suffix) : suffix.length), 0);
16888
16894
  filename = options.filenameMaxLengthUnit == "bytes" ? await truncateText(filename, maxLength) : filename.substring(0, maxLength);
package/package.json CHANGED
@@ -1,9 +1,16 @@
1
1
  {
2
2
  "name": "single-file-core",
3
- "version": "1.6.1",
3
+ "version": "1.6.3",
4
4
  "description": "SingleFile Core",
5
5
  "author": "Gildas Lormeau",
6
6
  "license": "AGPL-3.0-or-later",
7
+ "files": [
8
+ "*.js",
9
+ "core/",
10
+ "modules/",
11
+ "processors/",
12
+ "vendor/"
13
+ ],
7
14
  "scripts": {
8
15
  "test": "deno run --allow-read --allow-run test/run.js",
9
16
  "bump-patch": "npm version patch --no-git-tag-version && npm run bump-commit",
@@ -21,6 +28,7 @@
21
28
  "homepage": "https://github.com/gildas-lormeau/single-file-core#readme",
22
29
  "devDependencies": {
23
30
  "@eslint/js": "^9.39.5",
24
- "eslint": "^10.9.1"
31
+ "eslint": "^10.9.1",
32
+ "happy-dom": "^20.14.5"
25
33
  }
26
34
  }
@@ -0,0 +1,28 @@
1
+ /*
2
+ * Copyright 2010-2022 Gildas Lormeau
3
+ * contact : gildas.lormeau <at> gmail.com
4
+ *
5
+ * This file is part of SingleFile.
6
+ *
7
+ * The code in this file is free software: you can redistribute it and/or
8
+ * modify it under the terms of the GNU Affero General Public License
9
+ * (GNU AGPL) as published by the Free Software Foundation, either version 3
10
+ * of the License, or (at your option) any later version.
11
+ *
12
+ * The code in this file is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
15
+ * General Public License for more details.
16
+ *
17
+ * As additional permission under GNU AGPL version 3 section 7, you may
18
+ * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
19
+ * AGPL normally required by section 4, provided you include this license
20
+ * notice and a URL through which recipients can access the Corresponding
21
+ * Source.
22
+ */
23
+
24
+ const DEFAULT_MAX_APPENDED_DATA_LENGTH = 16361;
25
+
26
+ export {
27
+ DEFAULT_MAX_APPENDED_DATA_LENGTH
28
+ };
@@ -39,6 +39,9 @@ import {
39
39
  import {
40
40
  router
41
41
  } from "./compression-router.js";
42
+ import {
43
+ DEFAULT_MAX_APPENDED_DATA_LENGTH
44
+ } from "./compression-constants.js";
42
45
 
43
46
  const { Blob, fetch, TextEncoder, DOMParser } = globalThis;
44
47
 
@@ -89,7 +92,6 @@ const PNG_SIGNATURE_LENGTH = 8;
89
92
  const PNG_IHDR_LENGTH = 25;
90
93
  const COMMENT_LENGTH_FIELD_LENGTH = 2;
91
94
  const MAX_ZIP_COMMENT_LENGTH = 65535;
92
- const DEFAULT_MAX_APPENDED_DATA_LENGTH = 16361;
93
95
  const PDF_ENTRY_FILENAME = "page.pdf";
94
96
  const PRESCAN_WINDOW_LENGTH = 1024;
95
97
  const PNG_TEXT_CHUNK_HEADER_LENGTH = 12;
@@ -1,88 +0,0 @@
1
- # .github/workflows/publish.yml
2
-
3
- name: Publish
4
-
5
- on:
6
- release:
7
- types: [published]
8
-
9
- permissions:
10
- id-token: write
11
- contents: read
12
-
13
- jobs:
14
- # nothing else guarantees that the released commit was verified: the vendor workflow runs on push,
15
- # so this waits for its run on that exact commit and refuses to publish unless it succeeded
16
- check_vendor:
17
- name: 'Check vendor rebuild'
18
- runs-on: ubuntu-latest
19
- permissions:
20
- contents: read
21
- actions: read
22
- steps:
23
- # only used to resolve the tag to a commit, so it runs no code from the repository and keeps no
24
- # git credentials behind
25
- - uses: actions/checkout@v5
26
- with:
27
- ref: ${{ github.event.release.tag_name }}
28
- persist-credentials: false
29
- # the tag is typed in the release UI, so nothing ties it to the version that is about to be
30
- # published: a mistyped tag, or a bump that was never pushed, publishes a version nobody asked for
31
- - name: Verify the tag matches the declared version
32
- env:
33
- TAG_NAME: ${{ github.event.release.tag_name }}
34
- run: |
35
- version=$(jq -r .version package.json)
36
- if [ "$TAG_NAME" != "v$version" ]; then
37
- echo "::error::release $TAG_NAME but package.json declares $version"
38
- exit 1
39
- fi
40
- - name: Wait for the vendor rebuild check of the released commit
41
- env:
42
- GH_TOKEN: ${{ github.token }}
43
- run: |
44
- sha=$(git rev-parse HEAD)
45
- echo "released commit: $sha"
46
- missing=0
47
- for attempt in $(seq 1 60); do
48
- run=$(gh api "repos/$GITHUB_REPOSITORY/actions/workflows/vendor.yml/runs?head_sha=$sha&per_page=1" \
49
- --jq 'if (.workflow_runs | length) == 0 then "none none none"
50
- else (.workflow_runs[0] | "\(.status) \(.conclusion) \(.html_url)") end')
51
- run_status=$(echo "$run" | cut -d' ' -f1)
52
- conclusion=$(echo "$run" | cut -d' ' -f2)
53
- url=$(echo "$run" | cut -d' ' -f3)
54
- if [ "$run_status" = "none" ]; then
55
- missing=$((missing + 1))
56
- if [ "$missing" -ge 10 ]; then
57
- echo "::error::no vendor run for $sha, run the vendor workflow on this commit and publish again"
58
- exit 1
59
- fi
60
- echo "no vendor run found yet for $sha"
61
- else
62
- echo "vendor run: status=$run_status conclusion=$conclusion $url"
63
- if [ "$run_status" = "completed" ]; then
64
- if [ "$conclusion" = "success" ]; then
65
- exit 0
66
- fi
67
- echo "::error::the vendor rebuild concluded $conclusion for $sha, see $url"
68
- exit 1
69
- fi
70
- fi
71
- sleep 30
72
- done
73
- echo "::error::timed out waiting for the vendor rebuild of $sha"
74
- exit 1
75
-
76
- publish_npm:
77
- needs: check_vendor
78
- runs-on: ubuntu-latest
79
- steps:
80
- - uses: actions/checkout@v5
81
- with:
82
- ref: ${{ github.event.release.tag_name }}
83
- # Setup .npmrc file to publish to npm
84
- - uses: actions/setup-node@v5
85
- with:
86
- node-version: '24'
87
- registry-url: 'https://registry.npmjs.org'
88
- - run: npm publish
@@ -1,31 +0,0 @@
1
- # .github/workflows/test.yml
2
-
3
- name: Test
4
-
5
- # tag pushes are excluded: creating a release would otherwise re-run the check already done on the branch
6
- on:
7
- push:
8
- branches: ['**']
9
- pull_request:
10
-
11
- jobs:
12
- sfz-harness:
13
- name: 'SFZ format rules'
14
- runs-on: ubuntu-latest
15
- steps:
16
- - uses: actions/checkout@v5
17
- - uses: denoland/setup-deno@v2
18
- with:
19
- deno-version: v2.x
20
- # the suites drive the writer directly, with no browser and no network
21
- - run: npm test
22
- lint:
23
- name: 'Lint'
24
- runs-on: ubuntu-latest
25
- steps:
26
- - uses: actions/checkout@v5
27
- - uses: actions/setup-node@v5
28
- with:
29
- node-version: '24'
30
- - run: npm ci
31
- - run: npx eslint .
@@ -1,39 +0,0 @@
1
- # .github/workflows/vendor.yml
2
-
3
- name: Vendor
4
-
5
- # tag pushes are excluded: creating a release would otherwise re-run the check already done on the branch
6
- on:
7
- push:
8
- branches: ['**']
9
- pull_request:
10
-
11
- jobs:
12
- rebuild:
13
- name: 'Rebuild vendor/zip'
14
- runs-on: ubuntu-latest
15
- steps:
16
- - uses: actions/checkout@v5
17
- - uses: actions/setup-node@v5
18
- with:
19
- node-version: '24'
20
- - run: npm ci
21
- working-directory: zip-build
22
- - run: npm run build
23
- working-directory: zip-build
24
- # the checked-in files in vendor/zip must be exactly what the sources in zip-build produce
25
- - run: git diff --exit-code vendor/zip
26
- rebuild-css-tree:
27
- name: 'Rebuild vendor/css-tree.js'
28
- runs-on: ubuntu-latest
29
- steps:
30
- - uses: actions/checkout@v5
31
- - uses: actions/setup-node@v5
32
- with:
33
- node-version: '24'
34
- - run: npm ci
35
- working-directory: css-tree-build
36
- - run: npm run build
37
- working-directory: css-tree-build
38
- # the checked-in file must be exactly what the sources in css-tree-build produce
39
- - run: git diff --exit-code vendor/css-tree.js
@@ -1,39 +0,0 @@
1
- # Rebuilding `vendor/css-tree.js`
2
-
3
- [`../vendor/css-tree.js`](../vendor/css-tree.js) is a custom build of
4
- [css-tree](https://www.npmjs.com/package/css-tree) with one patch applied to
5
- `lib/tokenizer/TokenStream.js`: the token offset field is widened from 24 to
6
- 27 bits (`OFFSET_MASK`/`TYPE_SHIFT`, with unsigned shifts for the token type).
7
- Upstream css-tree packs each token as `type << 24 | offset`, so any stylesheet
8
- larger than 16MB (2^24 bytes) silently corrupts the token stream and parsing
9
- never terminates. Real pages ship such stylesheets (see
10
- https://github.com/gildas-lormeau/SingleFile/issues/1962, a 23.5MB stylesheet
11
- on brookings.edu). With 27 bits the limit becomes 128MB.
12
-
13
- This directory rebuilds the file deterministically. All dependencies are
14
- pinned to exact versions by `package.json` and `package-lock.json`:
15
-
16
- ```sh
17
- cd css-tree-build
18
- npm ci
19
- npm run build
20
- git diff --exit-code ../vendor/css-tree.js
21
- ```
22
-
23
- An empty diff means the checked-in file matches what the sources produce. The
24
- GitHub Actions workflow `.github/workflows/vendor.yml` runs the same steps on
25
- every push.
26
-
27
- The patch is applied to the readable npm sources by an esbuild plugin in
28
- `build.js`, guarded by exact occurrence counts: if a css-tree update changes
29
- `TokenStream.js`, the build fails loudly instead of silently dropping the
30
- patch.
31
-
32
- ## Updating css-tree
33
-
34
- 1. Bump `css-tree` in `package.json` and run `npm install`.
35
- 2. Run `npm run build`. If the build fails because `TokenStream.js` changed,
36
- review the patch in `build.js` against the new sources (or drop it once
37
- upstream stores offsets wider than 24 bits).
38
- 3. Commit the regenerated `../vendor/css-tree.js` together with the changes in
39
- this directory.
@@ -1,66 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { createRequire } from "node:module";
3
- import { build } from "esbuild";
4
-
5
- const require = createRequire(import.meta.url);
6
- const packagePath = require.resolve("css-tree/package.json");
7
- const licensePath = packagePath.replace(/package\.json$/, "LICENSE");
8
- const { version } = JSON.parse(readFileSync(packagePath));
9
-
10
- const PATCHES = [
11
- ["const OFFSET_MASK = 0x00FFFFFF;", "const OFFSET_MASK = 0x07FFFFFF;", 1],
12
- ["const TYPE_SHIFT = 24;", "const TYPE_SHIFT = 27;", 1],
13
- [">> TYPE_SHIFT", ">>> TYPE_SHIFT", 8]
14
- ];
15
-
16
- function patchTokenStream(source) {
17
- for (const [pattern, replacement, expectedCount] of PATCHES) {
18
- const count = source.split(pattern).length - 1;
19
- if (count != expectedCount) {
20
- throw new Error(`css-tree update changed TokenStream.js: found ${count} occurrence(s) of ${JSON.stringify(pattern)}, expected ${expectedCount} — review the 16MB token-offset patch`);
21
- }
22
- source = source.replaceAll(pattern, replacement);
23
- }
24
- return source;
25
- }
26
-
27
- const patchPlugin = {
28
- name: "patch-token-stream",
29
- setup(build) {
30
- let patched = false;
31
- build.onLoad({ filter: /[/\\]css-tree[/\\].*[/\\]TokenStream\.js$/ }, args => {
32
- patched = true;
33
- return { contents: patchTokenStream(readFileSync(args.path, "utf8")), loader: "js" };
34
- });
35
- build.onEnd(() => {
36
- if (!patched) {
37
- throw new Error("TokenStream.js was not loaded — the 16MB token-offset patch was not applied");
38
- }
39
- });
40
- }
41
- };
42
-
43
- const banner = [
44
- `// css-tree ${version} (https://github.com/csstree/csstree), bundled by css-tree-build/build.js`,
45
- "// with the token offset field widened from 24 to 27 bits so stylesheets larger",
46
- "// than 16MB do not corrupt the token stream (parsing never terminated on them)",
47
- "",
48
- "/*",
49
- readFileSync(licensePath, "utf8").trim().split("\n").map(line => (" * " + line).trimEnd()).join("\n"),
50
- " */"
51
- ].join("\n");
52
-
53
- await build({
54
- stdin: {
55
- contents: "export * from \"css-tree\";",
56
- resolveDir: import.meta.dirname
57
- },
58
- bundle: true,
59
- minify: true,
60
- format: "esm",
61
- platform: "browser",
62
- legalComments: "none",
63
- banner: { js: banner },
64
- plugins: [patchPlugin],
65
- outfile: "../vendor/css-tree.js"
66
- });