From 7bda0fc97c81fd0c642391acab439dd545be9f2e Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Mon, 25 May 2026 14:31:52 +0200 Subject: [PATCH 1/4] Enable 'eslint-plugin-regexp' and fix existing findings Enable the recommended preset and fix or per-line-disable the 78 findings it surfaces. Most are equivalent rewrites, intentional patterns (control chars, the whatwg email regex, autolinker URL regex) keep their behavior via targeted disables. --- eslint.config.mjs | 9 ++++ extensions/chromium/contentscript.js | 2 +- external/builder/builder.mjs | 3 +- external/check_l10n/check_l10n.mjs | 2 +- external/cmapscompress/parse.mjs | 4 +- gulpfile.mjs | 2 +- package-lock.json | 75 ++++++++++++++++++++++++++++ package.json | 1 + src/core/core_utils.js | 2 +- src/core/document.js | 2 +- src/core/evaluator.js | 4 +- src/core/font_substitutions.js | 4 +- src/core/fonts.js | 8 +-- src/core/postscript/lexer.js | 2 +- src/core/string_utils.js | 7 ++- src/core/unicode.js | 2 +- src/core/xfa/config.js | 2 +- src/core/xfa/fonts.js | 2 +- src/core/xfa/formcalc_lexer.js | 8 +-- src/core/xfa/xhtml.js | 3 +- src/display/annotation_layer.js | 6 +-- src/display/content_disposition.js | 6 ++- src/display/display_utils.js | 4 +- src/display/network.js | 2 +- src/scripting_api/aform.js | 9 ++-- src/scripting_api/util.js | 1 + src/shared/util.js | 2 +- test/downloadutils.mjs | 2 +- test/font/font_fpgm_spec.js | 2 +- test/font/font_glyf_spec.js | 4 +- test/integration/text_layer_spec.mjs | 2 +- test/resources/reftest-analyzer.js | 4 +- test/test.mjs | 2 +- test/unit/custom_spec.js | 2 +- web/app.js | 2 +- web/app_options.js | 2 +- web/autolinker.js | 3 +- web/chromecom.js | 4 +- web/internal/font_view.js | 5 +- web/pdf_find_controller.js | 6 +-- web/ui_utils.js | 1 + 41 files changed, 152 insertions(+), 63 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 082f277742c62..1042adc92124d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -7,6 +7,7 @@ import noUnsanitized from "eslint-plugin-no-unsanitized"; import perfectionist from "eslint-plugin-perfectionist"; import preferMathClamp from "./external/eslint_plugins/prefer-math-clamp.mjs"; import prettierRecommended from "eslint-plugin-prettier/recommended"; +import regexpPlugin from "eslint-plugin-regexp"; import unicorn from "eslint-plugin-unicorn"; const jsFiles = folder => { @@ -55,6 +56,14 @@ export default [ \* ======================================================================== */ prettierRecommended, + { + files: jsFiles("."), + plugins: regexpPlugin.configs["flat/recommended"].plugins, + rules: { + ...regexpPlugin.configs["flat/recommended"].rules, + "regexp/no-legacy-features": "off", + }, + }, { files: ["**/*.json", "**/.*.json"], language: "json/json", diff --git a/extensions/chromium/contentscript.js b/extensions/chromium/contentscript.js index 83ebf96a225c7..87573ab7048b5 100644 --- a/extensions/chromium/contentscript.js +++ b/extensions/chromium/contentscript.js @@ -45,7 +45,7 @@ function watchObjectOrEmbed(elem) { // var srcAttribute = "src" in elem ? "src" : "data"; var path = elem[srcAttribute]; - if (!mimeType && !/\.pdf($|[?#])/i.test(path)) { + if (!mimeType && !/\.pdf(?:$|[?#])/i.test(path)) { return; } diff --git a/external/builder/builder.mjs b/external/builder/builder.mjs index 6d35c950e7420..c212d01f99eda 100644 --- a/external/builder/builder.mjs +++ b/external/builder/builder.mjs @@ -132,7 +132,7 @@ function preprocess(inFilename, outFilename, defines) { } } function expand(line) { - line = line.replaceAll(/__[\w]+__/g, function (variable) { + line = line.replaceAll(/__\w+__/g, function (variable) { variable = variable.substring(2, variable.length - 2); if (variable in defines) { return defines[variable]; @@ -158,6 +158,7 @@ function preprocess(inFilename, outFilename, defines) { let state = STATE_NONE; const stack = []; const control = + // eslint-disable-next-line regexp/no-super-linear-backtracking /^(?:\/\/|\s*\/\*|\s*)?$)?/; while ((line = readLine()) !== null) { diff --git a/external/check_l10n/check_l10n.mjs b/external/check_l10n/check_l10n.mjs index 8dae8d6f8824c..f027e8bd6b73f 100644 --- a/external/check_l10n/check_l10n.mjs +++ b/external/check_l10n/check_l10n.mjs @@ -43,7 +43,7 @@ function extractFtlIds(ftlPath) { const lines = readFileSync(ftlPath, "utf8").split("\n"); const ids = []; for (const line of lines) { - const match = line.match(/^([a-zA-Z][a-zA-Z0-9-]*)\s*=/); + const match = line.match(/^([a-z][a-z0-9-]*)\s*=/i); if (match) { ids.push(match[1]); } diff --git a/external/cmapscompress/parse.mjs b/external/cmapscompress/parse.mjs index 47ec49c01533f..3fe93bff8ed10 100644 --- a/external/cmapscompress/parse.mjs +++ b/external/cmapscompress/parse.mjs @@ -14,7 +14,7 @@ */ function parseAdobeCMap(content) { - let m = /(\bbegincmap\b[\s\S]*?)\bendcmap\b/.exec(content); + let m = /(\bbegincmap\b[\s\S]+?)\bendcmap\b/.exec(content); if (!m) { throw new Error("cmap was not found"); } @@ -37,7 +37,7 @@ function parseAdobeCMap(content) { result.usecmap = m[1]; } const re = - /(\d+)\s+(begincodespacerange|beginnotdefrange|begincidchar|begincidrange|beginbfchar|beginbfrange)\n([\s\S]*?)\n(endcodespacerange|endnotdefrange|endcidchar|endcidrange|endbfchar|endbfrange)/g; + /(\d+)\s+(begincodespacerange|beginnotdefrange|begincidchar|begincidrange|beginbfchar|beginbfrange)\n([\s\S]*?)\n(?:endcodespacerange|endnotdefrange|endcidchar|endcidrange|endbfchar|endbfrange)/g; while ((m = re.exec(body))) { const lines = m[3].toLowerCase().split("\n"); diff --git a/gulpfile.mjs b/gulpfile.mjs index d06573055068b..0d208c79c3d50 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -1135,7 +1135,7 @@ gulp.task("locale", function () { if (!checkDir(dirPath)) { continue; } - if (!/^[a-z][a-z]([a-z])?(-[A-Z][A-Z])?$/.test(locale)) { + if (!/^[a-z]{2,3}(?:-[A-Z]{2})?$/.test(locale)) { console.log("Skipping invalid locale: " + locale); continue; } diff --git a/package-lock.json b/package-lock.json index c1c8b052dde95..f8541e304ea64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "eslint-plugin-no-unsanitized": "^4.1.5", "eslint-plugin-perfectionist": "^5.9.0", "eslint-plugin-prettier": "^5.5.5", + "eslint-plugin-regexp": "^3.1.0", "eslint-plugin-unicorn": "^64.0.0", "globals": "^17.6.0", "gulp": "^5.0.1", @@ -5462,6 +5463,28 @@ } } }, + "node_modules/eslint-plugin-regexp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-3.1.0.tgz", + "integrity": "sha512-qGXIC3DIKZHcK1H9A9+Byz9gmndY6TTSRkSMTZpNXdyCw2ObSehRgccJv35n9AdUakEjQp5VFNLas6BMXizCZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.11.0", + "comment-parser": "^1.4.0", + "jsdoc-type-pratt-parser": "^7.0.0", + "refa": "^0.12.1", + "regexp-ast-analysis": "^0.7.1", + "scslre": "^0.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "peerDependencies": { + "eslint": ">=9.38.0" + } + }, "node_modules/eslint-plugin-unicorn": { "version": "64.0.0", "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-64.0.0.tgz", @@ -7378,6 +7401,16 @@ "node": ">=12.0.0" } }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-7.2.0.tgz", + "integrity": "sha512-dh140MMgjyg3JhJZY/+iEzW+NO5xR2gpbDFKHqotCmexElVntw7GjWjt511+C/Ef02RU5TKYrJo/Xlzk+OLaTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/jsdoc/node_modules/escape-string-regexp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", @@ -9092,6 +9125,19 @@ "node": ">= 10.13.0" } }, + "node_modules/refa": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/refa/-/refa-0.12.1.tgz", + "integrity": "sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.8.0" + }, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -9112,6 +9158,20 @@ "node": ">=4" } }, + "node_modules/regexp-ast-analysis": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/regexp-ast-analysis/-/regexp-ast-analysis-0.7.1.tgz", + "integrity": "sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.8.0", + "refa": "^0.12.1" + }, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, "node_modules/regexp-tree": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", @@ -9446,6 +9506,21 @@ "dev": true, "license": "MIT" }, + "node_modules/scslre": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/scslre/-/scslre-0.3.0.tgz", + "integrity": "sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.8.0", + "refa": "^0.12.0", + "regexp-ast-analysis": "^0.7.0" + }, + "engines": { + "node": "^14.0.0 || >=16.0.0" + } + }, "node_modules/section-matter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", diff --git a/package.json b/package.json index b9b5c60f99f72..b737297a640ef 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "eslint-plugin-no-unsanitized": "^4.1.5", "eslint-plugin-perfectionist": "^5.9.0", "eslint-plugin-prettier": "^5.5.5", + "eslint-plugin-regexp": "^3.1.0", "eslint-plugin-unicorn": "^64.0.0", "globals": "^17.6.0", "gulp": "^5.0.1", diff --git a/src/core/core_utils.js b/src/core/core_utils.js index 7397781813a5c..9d28670af25e1 100644 --- a/src/core/core_utils.js +++ b/src/core/core_utils.js @@ -564,7 +564,7 @@ function validateFontName(fontFamily, mustWarn = false) { } else { // See https://developer.mozilla.org/en-US/docs/Web/CSS/custom-ident. for (const ident of fontFamily.split(/[ \t]+/)) { - if (/^(\d|(-(\d|-)))/.test(ident) || !/^[\w-\\]+$/.test(ident)) { + if (/^(?:\d|-[\d-])/.test(ident) || !/^[\w\\-]+$/.test(ident)) { if (mustWarn) { warn(`FontFamily contains invalid : ${fontFamily}.`); } diff --git a/src/core/document.js b/src/core/document.js index 20fdb012fc148..7b96d6c95cacc 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -1392,7 +1392,7 @@ class PDFDocument { } let fontFamily = descriptor.get("FontFamily"); // For example, "Wingdings 3" is not a valid font name in the css specs. - fontFamily = fontFamily.replaceAll(/[ ]+(\d)/g, "$1"); + fontFamily = fontFamily.replaceAll(/ +(\d)/g, "$1"); const fontWeight = descriptor.get("FontWeight"); // Angle is expressed in degrees counterclockwise in PDF diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 6961d2b0021e0..9e0952dc0e1bd 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -4203,9 +4203,7 @@ class PartialEvaluator { isSerifFont(baseFontName) { // Simulating descriptor flags attribute const fontNameWoStyle = baseFontName.split("-", 1)[0]; - return ( - fontNameWoStyle in getSerifFonts() || /serif/gi.test(fontNameWoStyle) - ); + return fontNameWoStyle in getSerifFonts() || /serif/i.test(fontNameWoStyle); } getBaseFontMetrics(name) { diff --git a/src/core/font_substitutions.js b/src/core/font_substitutions.js index 7325087b3a935..55aead624b9e0 100644 --- a/src/core/font_substitutions.js +++ b/src/core/font_substitutions.js @@ -579,8 +579,8 @@ function getFontSubstitution( return null; } // Maybe we'll be lucky and the OS will have the font. - const bold = /bold/gi.test(baseFontName); - const italic = /oblique|italic/gi.test(baseFontName); + const bold = /bold/i.test(baseFontName); + const italic = /oblique|italic/i.test(baseFontName); const style = (bold && italic && BOLDITALIC) || (bold && BOLD) || diff --git a/src/core/fonts.js b/src/core/fonts.js index 46528c316f55f..720d229a5862c 100644 --- a/src/core/fonts.js +++ b/src/core/fonts.js @@ -1235,16 +1235,16 @@ class Font { } } - this.bold = /bold/gi.test(fontName); - this.italic = /oblique|italic/gi.test(fontName); + this.bold = /bold/i.test(fontName); + this.italic = /oblique|italic/i.test(fontName); // Use 'name' instead of 'fontName' here because the original // name ArialBlack for example will be replaced by Helvetica. - this.black = /Black/g.test(name); + this.black = /Black/.test(name); // Use 'name' instead of 'fontName' here because the original // name ArialNarrow for example will be replaced by Helvetica. - const isNarrow = /Narrow/g.test(name); + const isNarrow = /Narrow/.test(name); // if at least one width is present, remeasure all chars when exists this.remeasure = diff --git a/src/core/postscript/lexer.js b/src/core/postscript/lexer.js index 8ac271dfe45bb..fa50551cd5083 100644 --- a/src/core/postscript/lexer.js +++ b/src/core/postscript/lexer.js @@ -132,7 +132,7 @@ class Lexer { this.pos = 0; this.len = data.length; // Sticky regexes: set lastIndex before exec() to match at an exact offset. - this._numberPattern = /[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/y; + this._numberPattern = /[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?/iy; this._identifierPattern = /[a-z]+/y; } diff --git a/src/core/string_utils.js b/src/core/string_utils.js index a34fdf911531d..53806c606e035 100644 --- a/src/core/string_utils.js +++ b/src/core/string_utils.js @@ -16,7 +16,11 @@ import { stringToBytes, Util, warn } from "../shared/util.js"; function isAscii(str) { - return typeof str === "string" && (!str || /^[\x00-\x7F]*$/.test(str)); + return ( + typeof str === "string" && + // eslint-disable-next-line no-control-regex + (!str || /^[\x00-\x7F]*$/.test(str)) + ); } // If the string is null or undefined then it is returned as is. @@ -91,6 +95,7 @@ function stringToPDFString(str, keepEscapeSequence = false) { if (keepEscapeSequence || !decoded.includes("\x1b")) { return decoded; } + // eslint-disable-next-line no-control-regex return decoded.replaceAll(/\x1b[^\x1b]*(?:\x1b|$)/g, ""); } catch (ex) { warn(`stringToPDFString: "${ex}".`); diff --git a/src/core/unicode.js b/src/core/unicode.js index c631fd3ed9f32..028b298c57cfc 100644 --- a/src/core/unicode.js +++ b/src/core/unicode.js @@ -242,7 +242,7 @@ function getUnicodeRangeFor(value, lastPosition = -1) { return -1; } -const SpecialCharRegExp = new RegExp("^(\\s)|(\\p{Mn})|(\\p{Cf})$", "u"); +const SpecialCharRegExp = /^(\s)|(\p{Mn})|(\p{Cf})$/u; const CategoryCache = new Map(); function getCharUnicodeCategory(char) { diff --git a/src/core/xfa/config.js b/src/core/xfa/config.js index 8e1dbc9d1b027..832933896ae1f 100644 --- a/src/core/xfa/config.js +++ b/src/core/xfa/config.js @@ -1003,7 +1003,7 @@ class Rename extends ContentObject { // is no colon. if ( this[$content].toLowerCase().startsWith("xml") || - new RegExp("[\\p{L}_][\\p{L}\\d._\\p{M}-]*", "u").test(this[$content]) + /[\p{L}_][\p{L}\d._\p{M}-]*/u.test(this[$content]) ) { warn("XFA - Rename: invalid XFA name"); } diff --git a/src/core/xfa/fonts.js b/src/core/xfa/fonts.js index 17572b97bf5ad..897b4bb40d091 100644 --- a/src/core/xfa/fonts.js +++ b/src/core/xfa/fonts.js @@ -90,7 +90,7 @@ class FontFinder { return font; } - const pattern = /,|-|_| |bolditalic|bold|italic|regular|it/gi; + const pattern = /[,\-_ ]|bolditalic|bold|italic|regular|it/gi; let name = fontName.replaceAll(pattern, ""); font = this.fonts.get(name); if (font) { diff --git a/src/core/xfa/formcalc_lexer.js b/src/core/xfa/formcalc_lexer.js index d6a4eafaee5ba..494ca1a837ce2 100644 --- a/src/core/xfa/formcalc_lexer.js +++ b/src/core/xfa/formcalc_lexer.js @@ -116,11 +116,11 @@ const TOKEN = { upto: 54, }; -const hexPattern = /^[uU]([0-9a-fA-F]{4,8})/; -const numberPattern = /^\d*(?:\.\d*)?(?:[Ee][+-]?\d+)?/; -const dotNumberPattern = /^\d*(?:[Ee][+-]?\d+)?/; +const hexPattern = /^u([0-9a-f]{4,8})/i; +const numberPattern = /^\d*(?:\.\d*)?(?:E[+-]?\d+)?/i; +const dotNumberPattern = /^\d*(?:E[+-]?\d+)?/i; const eolPattern = /[\r\n]+/; -const identifierPattern = new RegExp("^[\\p{L}_$!][\\p{L}\\p{N}_$]*", "u"); +const identifierPattern = /^[\p{L}_$!][\p{L}\p{N}_$]*/u; class Token { constructor(id, value = null) { diff --git a/src/core/xfa/xhtml.js b/src/core/xfa/xhtml.js index 5998829f9015f..d0f1cf34950c4 100644 --- a/src/core/xfa/xhtml.js +++ b/src/core/xfa/xhtml.js @@ -134,8 +134,7 @@ function mapStyle(styleStr, node, richText) { ? `${style[key]} ${newValue}` : newValue; } else { - style[key.replaceAll(/-([a-zA-Z])/g, (_, x) => x.toUpperCase())] = - newValue; + style[key.replaceAll(/-([a-z])/gi, (_, x) => x.toUpperCase())] = newValue; } } diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index ec2999b3b1741..c7bd5810fafb0 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -1793,16 +1793,14 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { case "deleteWordBackward": { const match = value .substring(0, selectionStart) - .match(/\w*[^\w]*$/); + .match(/\w*\W*$/); if (match) { selStart -= match[0].length; } break; } case "deleteWordForward": { - const match = value - .substring(selectionStart) - .match(/^[^\w]*\w*/); + const match = value.substring(selectionStart).match(/^\W*\w*/); if (match) { selEnd += match[0].length; } diff --git a/src/display/content_disposition.js b/src/display/content_disposition.js index 913a44d75d357..b774452151448 100644 --- a/src/display/content_disposition.js +++ b/src/display/content_disposition.js @@ -81,6 +81,7 @@ function getFilenameFromContentDispositionHeader(contentDisposition) { } function textdecode(encoding, value) { if (encoding) { + // eslint-disable-next-line no-control-regex if (!/^[\x00-\xFF]+$/.test(value)) { return value; } @@ -184,6 +185,7 @@ function getFilenameFromContentDispositionHeader(contentDisposition) { // Firefox also decodes words even where RFC 2047 section 5 states: // "An 'encoded-word' MUST NOT appear within a 'quoted-string'." + // eslint-disable-next-line no-control-regex if (!value.startsWith("=?") || /[\x00-\x19\x80-\xff]/.test(value)) { return value; } @@ -195,12 +197,12 @@ function getFilenameFromContentDispositionHeader(contentDisposition) { // encoded-text = any printable ASCII character other than ? or space. // ... but Firefox permits ? and space. return value.replaceAll( - /=\?([\w-]*)\?([QqBb])\?((?:[^?]|\?(?!=))*)\?=/g, + /=\?([\w-]*)\?([QB])\?((?:[^?]|\?(?!=))*)\?=/gi, function (matches, charset, encoding, text) { if (encoding === "q" || encoding === "Q") { // RFC 2047 section 4.2. text = text.replaceAll("_", " "); - text = text.replaceAll(/=([0-9a-fA-F]{2})/g, function (match, hex) { + text = text.replaceAll(/=([0-9a-f]{2})/gi, function (match, hex) { return String.fromCharCode(parseInt(hex, 16)); }); return textdecode(charset, text); diff --git a/src/display/display_utils.js b/src/display/display_utils.js index b64ccce103ce1..5fc5815a37a09 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -296,7 +296,7 @@ class PDFDateString { "(\\d{2})?" + // Hour (optional) "(\\d{2})?" + // Minute (optional) "(\\d{2})?" + // Second (optional) - "([Z|+|-])?" + // Universal time relation (optional) + "([Z|+\\-])?" + // Universal time relation (optional) "(\\d{2})?" + // Offset hour (optional) "'?" + // Splitting apostrophe (optional) "(\\d{2})?" + // Offset minute (optional) @@ -756,7 +756,7 @@ function renderRichText({ html, dir, className }, container) { if (typeof html === "string") { const p = document.createElement("p"); p.dir = dir || "auto"; - const lines = html.split(/(?:\r\n?|\n)/); + const lines = html.split(/\r\n?|\n/); for (let i = 0, ii = lines.length; i < ii; ++i) { const line = lines[i]; p.append(document.createTextNode(line)); diff --git a/src/display/network.js b/src/display/network.js index a4d94be884903..471203828a043 100644 --- a/src/display/network.js +++ b/src/display/network.js @@ -140,7 +140,7 @@ class PDFNetworkStream extends BasePDFStream { const chunk = getArrayBuffer(xhr.response); if (xhrStatus === PARTIAL_CONTENT_RESPONSE) { const rangeHeader = xhr.getResponseHeader("Content-Range"); - if (/bytes (\d+)-(\d+)\/(\d+)/.test(rangeHeader)) { + if (/bytes \d+-\d+\/\d+/.test(rangeHeader)) { pendingRequest.onDone(chunk); } else { warn(`Missing or invalid "Content-Range" header.`); diff --git a/src/scripting_api/aform.js b/src/scripting_api/aform.js index d390044457988..26969d8de8edd 100644 --- a/src/scripting_api/aform.js +++ b/src/scripting_api/aform.js @@ -26,8 +26,9 @@ class AForm { // The e-mail address regex below originates from: // https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address + // eslint-disable-next-line regexp/use-ignore-case this._emailRegex = new RegExp( - "^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+" + + "^[\\w.!#$%&'*+/=?^`{|}~-]+" + "@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?" + "(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$" ); @@ -184,12 +185,12 @@ class AForm { // comma sep pattern = event.willCommit ? /^[+-]?(\d+(,\d*)?|,\d+)$/ - : /^[+-]?\d*,?\d*$/; + : /^[+-]?\d*(?:,\d*)?$/; } else { // dot sep pattern = event.willCommit ? /^[+-]?(\d+(\.\d*)?|\.\d+)$/ - : /^[+-]?\d*\.?\d*$/; + : /^[+-]?\d*(?:\.\d*)?$/; } if (!pattern.test(value)) { @@ -571,7 +572,7 @@ class AForm { event.rc = true; } - const re = /([-()]|\s)+/g; + const re = /[-()\s]+/g; value = value.replaceAll(re, ""); for (const format of formats) { this.#AFSpecial_KeystrokeEx_helper( diff --git a/src/scripting_api/util.js b/src/scripting_api/util.js index 5d576e01c90fe..7aeb79737bca4 100644 --- a/src/scripting_api/util.js +++ b/src/scripting_api/util.js @@ -62,6 +62,7 @@ class Util extends PDFObject { throw new TypeError("First argument of printf must be a string"); } + // eslint-disable-next-line regexp/no-misleading-capturing-group const pattern = /%(,[0-4])?([+ 0#]+)?(\d+)?(\.\d+)?(.)/g; const PLUS = 1; const SPACE = 2; diff --git a/src/shared/util.js b/src/shared/util.js index 6c36f758a4665..1ae18fbeef9c0 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -1039,7 +1039,7 @@ function normalizeUnicode(str) { // required. // It appears that most the chars here contain some ligatures. NormalizeRegex = - /([\u00a0\u00b5\u037e\u0eb3\u2000-\u200a\u202f\u2126\ufb00-\ufb04\ufb06\ufb20-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufba1\ufba4-\ufba9\ufbae-\ufbb1\ufbd3-\ufbdc\ufbde-\ufbe7\ufbea-\ufbf8\ufbfc-\ufbfd\ufc00-\ufc5d\ufc64-\ufcf1\ufcf5-\ufd3d\ufd88\ufdf4\ufdfa-\ufdfb\ufe71\ufe77\ufe79\ufe7b\ufe7d]+)|(\ufb05+)/gu; + /([\u00a0\u00b5\u037e\u0eb3\u2000-\u200a\u202f\u2126\ufb00-\ufb04\ufb06\ufb20-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufba1\ufba4-\ufba9\ufbae-\ufbb1\ufbd3-\ufbdc\ufbde-\ufbe7\ufbea-\ufbf8\ufbfc\ufbfd\ufc00-\ufc5d\ufc64-\ufcf1\ufcf5-\ufd3d\ufd88\ufdf4\ufdfa\ufdfb\ufe71\ufe77\ufe79\ufe7b\ufe7d]+)|(\ufb05+)/gu; NormalizationMap = new Map([["ſt", "ſt"]]); } return str.replaceAll(NormalizeRegex, (_, p1, p2) => diff --git a/test/downloadutils.mjs b/test/downloadutils.mjs index feab36d53bb42..1356151977ea3 100644 --- a/test/downloadutils.mjs +++ b/test/downloadutils.mjs @@ -21,7 +21,7 @@ function rewriteWebArchiveUrl(url) { // Without this, an HTML page containing an iframe with the PDF file // will be served instead (issue 8920). const webArchiveRegex = - /(^https?:\/\/web\.archive\.org\/web\/)(\d+)(\/https?:\/\/.+)/g; + /(^https?:\/\/web\.archive\.org\/web\/)(\d+)(\/https?:\/\/.+)/; const urlParts = webArchiveRegex.exec(url); if (urlParts) { return `${urlParts[1]}${urlParts[2]}if_${urlParts[3]}`; diff --git a/test/font/font_fpgm_spec.js b/test/font/font_fpgm_spec.js index ed8605ef4da28..164fd1222668a 100644 --- a/test/font/font_fpgm_spec.js +++ b/test/font/font_fpgm_spec.js @@ -36,7 +36,7 @@ describe("font_fpgm", function () { verifyTtxOutput(output); expect( - /(ENDF\[ \]|SVTCA\[0\])\s*\/\*.*\*\/\s*<\/assembly>\s*<\/fpgm>/.test( + /(?:ENDF\[ \]|SVTCA\[0\])\s*\/\*.*\*\/\s*<\/assembly>\s*<\/fpgm>/.test( output ) ).toEqual(true); diff --git a/test/font/font_glyf_spec.js b/test/font/font_glyf_spec.js index decaedccd96ae..17b996347e410 100644 --- a/test/font/font_glyf_spec.js +++ b/test/font/font_glyf_spec.js @@ -109,7 +109,7 @@ describe("font_glyf", function () { ); expect(notdef).not.toBeNull(); expect(notdef[1] || "").not.toMatch( - /]*glyphName="\.notdef"/ + /]+glyphName="\.notdef"/ ); }); }); @@ -132,7 +132,7 @@ describe("font_glyf", function () { const output = await ttx(font.data); verifyTtxOutput(output); expect( - /\s*(\s*)?/.test(output) + /\s*(?:\s*)?/.test(output) ).toEqual(true); expect(/ { // Selection starts mid-word in Heading 1, so assert the stable // trailing content rather than exact full-line boundaries. .toHaveRoughlySelected( - /ing 1\s+This paragraph 1\.\s+Heading 2\s+This paragraph 2/s + /ing 1\s+This paragraph 1\.\s+Heading 2\s+This paragraph 2/ ); }) ); diff --git a/test/resources/reftest-analyzer.js b/test/resources/reftest-analyzer.js index 3972c1d376f61..bd69d4dfc33ca 100644 --- a/test/resources/reftest-analyzer.js +++ b/test/resources/reftest-analyzer.js @@ -205,7 +205,7 @@ window.onload = function () { } line = match[1]; match = line.match( - /^(TEST-PASS|TEST-UNEXPECTED-PASS|TEST-KNOWN-FAIL|TEST-UNEXPECTED-FAIL)(\(EXPECTED RANDOM\)|) \| ([^|]+) \|(.*)/ + /^(TEST-PASS|TEST-UNEXPECTED-PASS|TEST-KNOWN-FAIL|TEST-UNEXPECTED-FAIL)(\(EXPECTED RANDOM\))? \| ([^|]+) \|(.*)/ ); if (match) { const state = match[1]; @@ -225,7 +225,7 @@ window.onload = function () { continue; } match = line.match( - /^ {2}IMAGE[^:]*\((\d+\.?\d*)x(\d+\.?\d*)x(\d+\.?\d*)\): (.*)$/ + /^ {2}IMAGE[^:]*\((\d+(?:\.\d*)?)x(\d+(?:\.\d*)?)x(\d+(?:\.\d*)?)\): (.*)$/ ); if (match) { const item = gTestItems.at(-1); diff --git a/test/test.mjs b/test/test.mjs index 83aaa5464aeed..ae8da4417a307 100644 --- a/test/test.mjs +++ b/test/test.mjs @@ -78,7 +78,7 @@ function parseOptions() { // Expand `-X=value` short-option forms into `["-X", "value"]` since // parseArgs only strips the `=` separator for long options (--foo=bar). const args = process.argv.slice(2).flatMap(arg => { - const m = arg.match(/^(-[a-zA-Z])=(.*)/s); + const m = arg.match(/^(-[a-z])=(.*)/is); return m ? [m[1], m[2]] : [arg]; }); const { values } = parseArgs({ diff --git a/test/unit/custom_spec.js b/test/unit/custom_spec.js index a49fc368f7d8c..355ded742cf45 100644 --- a/test/unit/custom_spec.js +++ b/test/unit/custom_spec.js @@ -91,7 +91,7 @@ describe("custom ownerDocument", function () { const checkFont = font => /g_d\d+_f1/.test(font.family); const checkFontFaceRule = rule => - /^@font-face {font-family:"g_d\d+_f1";src:/.test(rule); + /^@font-face \{font-family:"g_d\d+_f1";src:/.test(rule); beforeEach(() => { globalThis.FontFace = function MockFontFace(name) { diff --git a/web/app.js b/web/app.js index 7b867114419f5..f45bb5c579888 100644 --- a/web/app.js +++ b/web/app.js @@ -1115,7 +1115,7 @@ const PDFViewerApplication = { // - The title may contain incorrectly encoded characters, which thus // looks broken, hence we ignore the Metadata entry when it contains // characters from the Specials Unicode block (fixes bug 1605526). - if (title !== "Untitled" && !/[\uFFF0-\uFFFF]/g.test(title)) { + if (title !== "Untitled" && !/[\uFFF0-\uFFFF]/.test(title)) { return title; } } diff --git a/web/app_options.js b/web/app_options.js index 27f24f2c3ce79..bdd365abaa970 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -19,7 +19,7 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { const isAndroid = /Android/.test(userAgent); const isIOS = - /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent) || + /\b(?:iPad|iPhone|iPod)(?=;)/.test(userAgent) || (platform === "MacIntel" && maxTouchPoints > 1); // Limit canvas size to 5 mega-pixels on mobile. diff --git a/web/autolinker.js b/web/autolinker.js index f0e6593873fa0..407025fb2c8b3 100644 --- a/web/autolinker.js +++ b/web/autolinker.js @@ -138,7 +138,8 @@ class Autolinker { static findLinks(text) { // Regex can be tested and verified at https://regex101.com/r/rXoLiT/2. this.#regex ??= - /\b(?:https?:\/\/|mailto:|www\.)(?:[\S--[\p{P}<>]]|\/|[\S--[\[\]]]+[\S--[\p{P}<>]])+|(?=\p{L})[\S--[@\p{Ps}\p{Pe}<>]]+@([\S--[[\p{P}--\-]<>]]+(?:\.[\S--[[\p{P}--\-]<>]]+)+)/gmv; + // eslint-disable-next-line regexp/no-super-linear-backtracking + /\b(?:https?:\/\/|mailto:|www\.)(?:[\S--[\p{P}<>]]|\/|[\S--[\[\]]]+[\S--[\p{P}<>]])+|(?=\p{L})[\S--[@\p{Ps}\p{Pe}<>]]+@([\S--[[\p{P}--\-]<>]]+(?:\.[\S--[[\p{P}--\-]<>]]+)+)/gv; const [normalizedText, diffs] = normalize(text, { ignoreDashEOL: true }); const matches = normalizedText.matchAll(this.#regex); diff --git a/web/chromecom.js b/web/chromecom.js index f62e4c02c5231..1ff5785566603 100644 --- a/web/chromecom.js +++ b/web/chromecom.js @@ -36,8 +36,8 @@ if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("CHROME")) { // Run this code outside DOMContentLoaded to make sure that the URL // is rewritten as soon as possible. const queryString = document.location.search.slice(1); - const m = /(^|&)file=([^&]*)/.exec(queryString); - let defaultUrl = m ? decodeURIComponent(m[2]) : ""; + const m = /(?:^|&)file=([^&]*)/.exec(queryString); + let defaultUrl = m ? decodeURIComponent(m[1]) : ""; if (!defaultUrl && queryString.startsWith("DNR:")) { // Redirected via DNR, see registerPdfRedirectRule in pdfHandler.js. defaultUrl = queryString.slice(4); diff --git a/web/internal/font_view.js b/web/internal/font_view.js index dbe304d4d3367..ccaf9c5443dce 100644 --- a/web/internal/font_view.js +++ b/web/internal/font_view.js @@ -115,10 +115,7 @@ class FontView { return; } const ext = MIMETYPE_TO_EXTENSION.get(font.mimetype) ?? "font"; - const name = (font.name || font.loadedName).replaceAll( - /[^a-z0-9_-]/gi, - "_" - ); + const name = (font.name || font.loadedName).replaceAll(/[^\w-]/g, "_"); const blob = new Blob([font.data], { type: font.mimetype }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); diff --git a/web/pdf_find_controller.js b/web/pdf_find_controller.js index 762929a11fd59..8582d7a84e70a 100644 --- a/web/pdf_find_controller.js +++ b/web/pdf_find_controller.js @@ -75,8 +75,8 @@ let DIACRITICS_EXCEPTION_STR; // Lazily initialized, see below. const DIACRITICS_REG_EXP = /\p{M}+/gu; const SPECIAL_CHARS_REG_EXP = /([+^$|])|(\p{P}+)|(\s+)|(\p{M})|(\p{L})/gu; -const NOT_DIACRITIC_FROM_END_REG_EXP = /([^\p{M}])\p{M}*$/u; -const NOT_DIACRITIC_FROM_START_REG_EXP = /^\p{M}*([^\p{M}])/u; +const NOT_DIACRITIC_FROM_END_REG_EXP = /(\P{M})\p{M}*$/u; +const NOT_DIACRITIC_FROM_START_REG_EXP = /^\p{M}*(\P{M})/u; // The range [AC00-D7AF] corresponds to the Hangul syllables. // The few other chars are some CJK Compatibility Ideographs. @@ -149,7 +149,7 @@ function normalize(text, options = {}) { ]; normalizationRegex = new RegExp( regexps.map(r => `(${r})`).join("|"), - "gum" + "gmu" ); if (hasSyllables) { diff --git a/web/ui_utils.js b/web/ui_utils.js index e72400ac9d73d..ee63c59bc8314 100644 --- a/web/ui_utils.js +++ b/web/ui_utils.js @@ -175,6 +175,7 @@ function parseQueryString(query) { return params; } +// eslint-disable-next-line no-control-regex const InvisibleCharsRegExp = /[\x00-\x1F]/g; /** From 8f85e3f20ba68986cad0943716b47d012fd3e70f Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Mon, 25 May 2026 14:44:42 +0200 Subject: [PATCH 2/4] Load the predefined CMap for composite fonts that omit the FontDescriptor and add font substitutions for the standard Acrobat CJK families. --- src/core/evaluator.js | 7 + src/core/font_substitutions.js | 192 +++++++++++++++++++++++++++ test/pdfs/.gitignore | 1 + test/pdfs/90ms_rksj_h_sample.pdf | Bin 0 -> 1039 bytes test/unit/api_spec.js | 19 +++ test/unit/font_substitutions_spec.js | 92 +++++++++++++ 6 files changed, 311 insertions(+) create mode 100644 test/pdfs/90ms_rksj_h_sample.pdf diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 6961d2b0021e0..628c196cd5a79 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -4436,6 +4436,13 @@ class PartialEvaluator { // FontDescriptor is only required for Type3 fonts when the document // is a tagged pdf. descriptor = Dict.empty; + } else if (composite) { + // Some PDFs omit the FontDescriptor on the descendant CIDFont when + // referencing one of the standard Acrobat CJK fonts via a predefined + // CMap (e.g. /Encoding /90ms-RKSJ-H with /BaseFont /HeiseiMin-W3). + // Fall through so the CMap is loaded by the composite-font path + // below; otherwise multi-byte codes would be decoded byte-by-byte. + descriptor = Dict.empty; } else { // Before PDF 1.5 if the font was one of the base 14 fonts, having a // FontDescriptor was not required. diff --git a/src/core/font_substitutions.js b/src/core/font_substitutions.js index 7325087b3a935..fc7b16c350b0f 100644 --- a/src/core/font_substitutions.js +++ b/src/core/font_substitutions.js @@ -21,6 +21,10 @@ const NORMAL = { style: "normal", weight: "normal", }; +const MEDIUM = { + style: "normal", + weight: "500", +}; const BOLD = { style: "normal", weight: "bold", @@ -364,6 +368,194 @@ const substitutionMap = new Map([ alias: "\xCB\xCE\xCC\xE5", }, ], + // Standard Acrobat CJK fonts. These BaseFont names appear in PDFs that + // don't embed a CJK font and rely on the reader having Acrobat's bundled + // CJK fonts installed. + // Adobe-Japan1 - Mincho (serif). + [ + "HeiseiMin-W3", + { + local: [ + "Hiragino Mincho ProN", + "Hiragino Mincho Pro", + "Yu Mincho", + "YuMincho", + "Source Han Serif JP", + "Noto Serif JP", + "Noto Serif CJK JP", + "IPAexMincho", + "IPAMincho", + "Takao Mincho", + "MS Mincho", + "MS PMincho", + ], + style: NORMAL, + ultimate: "serif", + }, + ], + // Adobe-Japan1 - Gothic (sans-serif). + [ + "HeiseiKakuGo-W5", + { + local: [ + "Hiragino Kaku Gothic ProN", + "Hiragino Kaku Gothic Pro", + "Hiragino Sans", + "Yu Gothic", + "YuGothic", + "Source Han Sans JP", + "Noto Sans JP", + "Noto Sans CJK JP", + "IPAexGothic", + "IPAGothic", + "Takao Gothic", + "Meiryo", + "MS Gothic", + "MS PGothic", + ], + style: MEDIUM, + ultimate: "sans-serif", + }, + ], + // Common Adobe-Japan1 variants and Kozuka names. + ["HeiseiMin-W3-Acro", { alias: "HeiseiMin-W3" }], + ["HeiseiKakuGo-W5-Acro", { alias: "HeiseiKakuGo-W5" }], + ["KozMinPro-Regular", { alias: "HeiseiMin-W3" }], + ["KozMinProVI-Regular", { alias: "HeiseiMin-W3" }], + ["KozMinPr6N-Regular", { alias: "HeiseiMin-W3" }], + ["KozGoPro-Regular", { alias: "HeiseiKakuGo-W5" }], + ["KozGoProVI-Regular", { alias: "HeiseiKakuGo-W5" }], + ["KozGoPr6N-Regular", { alias: "HeiseiKakuGo-W5" }], + + // Adobe-GB1 - Song (Simplified Chinese serif). + [ + "STSong-Light", + { + local: [ + "STSong", + "Songti SC", + "Source Han Serif SC", + "Source Han Serif CN", + "Noto Serif SC", + "Noto Serif CJK SC", + "AR PL UMing CN", + "SimSun", + "NSimSun", + ], + style: NORMAL, + ultimate: "serif", + }, + ], + // Adobe-GB1 - Hei (Simplified Chinese sans-serif). + [ + "STHeiti-Regular", + { + local: [ + "STHeiti", + "Heiti SC", + "PingFang SC", + "Source Han Sans SC", + "Source Han Sans CN", + "Noto Sans SC", + "Noto Sans CJK SC", + "Microsoft YaHei", + "SimHei", + "WenQuanYi Zen Hei", + ], + style: NORMAL, + ultimate: "sans-serif", + }, + ], + ["STSongStd-Light", { alias: "STSong-Light" }], + ["AdobeSongStd-Light", { alias: "STSong-Light" }], + ["AdobeHeitiStd-Regular", { alias: "STHeiti-Regular" }], + // KaiTi (regular script) and FangSong (imitation Song) are different + // typographic styles; route to the existing GB2312-keyed entries above. + ["AdobeKaitiStd-Regular", { alias: "\xBF\xAC\xCC\xE5" }], + ["AdobeFangsongStd-Regular", { alias: "\xB7\xC2\xCB\xCE" }], + + // Adobe-CNS1 - Sung (Traditional Chinese serif). + [ + "MSung-Light", + { + local: [ + "Songti TC", + "LiSong Pro", + "Source Han Serif TC", + "Source Han Serif TW", + "Noto Serif TC", + "Noto Serif CJK TC", + "AR PL UMing TW", + "PMingLiU", + "MingLiU", + "MingLiU_HKSCS", + ], + style: NORMAL, + ultimate: "serif", + }, + ], + // Adobe-CNS1 - Hei (Traditional Chinese sans-serif). + [ + "MHei-Medium", + { + local: [ + "Heiti TC", + "STHeiti", + "Source Han Sans TC", + "Source Han Sans TW", + "Noto Sans TC", + "Noto Sans CJK TC", + "PingFang TC", + "Microsoft JhengHei", + ], + style: MEDIUM, + ultimate: "sans-serif", + }, + ], + ["MSungStd-Light", { alias: "MSung-Light" }], + ["AdobeMingStd-Light", { alias: "MSung-Light" }], + + // Adobe-Korea1 - Myeongjo (Korean serif). + [ + "HYSMyeongJo-Medium", + { + local: [ + "AppleMyungjo", + "Source Han Serif KR", + "Noto Serif KR", + "Noto Serif CJK KR", + "Nanum Myeongjo", + "Batang", + ], + style: MEDIUM, + ultimate: "serif", + }, + ], + // Adobe-Korea1 - Gothic (Korean sans-serif). + [ + "HYGoThic-Medium", + { + local: [ + "Apple SD Gothic Neo", + "AppleGothic", + "Source Han Sans KR", + "Noto Sans KR", + "Noto Sans CJK KR", + "Nanum Gothic", + "Malgun Gothic", + "Dotum", + "Gulim", + ], + style: MEDIUM, + ultimate: "sans-serif", + }, + ], + ["HYSMyeongJoStd-Medium", { alias: "HYSMyeongJo-Medium" }], + ["AdobeMyungjoStd-Medium", { alias: "HYSMyeongJo-Medium" }], + // Bold variants reuse the same fallback list with a bold style override + // so the @font-face declaration requests a bold local() match. + ["HYGoThic-Bold", { alias: "HYGoThic-Medium", style: BOLD }], + ["AdobeGothicStd-Bold", { alias: "HYGoThic-Medium", style: BOLD }], ]); const fontAliases = new Map([["Arial-Black", "ArialBlack"]]); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index e866a0f1f0e58..3da556ba18b8f 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -924,3 +924,4 @@ !Embedded_font.pdf !issue18548_reduced.pdf !issue_cff_unsigned_bbox.pdf +!90ms_rksj_h_sample.pdf diff --git a/test/pdfs/90ms_rksj_h_sample.pdf b/test/pdfs/90ms_rksj_h_sample.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9bd93307be15a053ae21638e1c9f5ec63cff8a23 GIT binary patch literal 1039 zcmah|%WmQ@6y5tP?goi2nAiykK|)A8Robaqkw`46E?i<5SKy%7jN19hS-8YWKXwX&k#urXt|54mYh@us z!W{a2rc6OZM!^3OH8Hr?g@xyc_%E2I+#3htG?K0^BT1-I$gP!v2UTb}EZ^aoBR>%# zbh_aS1E8G7A=5&eL_9QCw%k`sG)fgrkx>6|h>3%cfR6v`_tDF#9ZGVr^HrI(`v%>R z2-a0fZrLzr!IXn&L%w1iv=MU13)Z|o%!w8E5bNULV|y3usdO*+GL zG#R8(5~op|;nL { + const fontName = "HeiseiMin-W3"; + const fontSubstitution = getFontSubstitution( + new Map(), + idFactory, + localFontPath, + fontName, + undefined, + "CIDFontType2" + ); + expect(fontSubstitution).toEqual( + jasmine.objectContaining({ + guessFallback: false, + baseFontName: "HeiseiMin-W3", + src: + "local(Hiragino Mincho ProN),local(Hiragino Mincho Pro)," + + "local(Yu Mincho),local(YuMincho),local(Source Han Serif JP)," + + "local(Noto Serif JP),local(Noto Serif CJK JP)," + + "local(IPAexMincho),local(IPAMincho),local(Takao Mincho)," + + "local(MS Mincho),local(MS PMincho)", + style: { + style: "normal", + weight: "normal", + }, + }) + ); + expect(fontSubstitution.css).toMatch( + /^"HeiseiMin W3",g_d(\d+)_sf(\d+),serif$/ + ); + }); + + it("should substitute a Kozuka Mincho alias", () => { + const fontName = "KozMinPr6N-Regular"; + const fontSubstitution = getFontSubstitution( + new Map(), + idFactory, + localFontPath, + fontName, + undefined, + "CIDFontType0" + ); + expect(fontSubstitution).toEqual( + jasmine.objectContaining({ + guessFallback: false, + baseFontName: "KozMinPr6N-Regular", + src: + "local(Hiragino Mincho ProN),local(Hiragino Mincho Pro)," + + "local(Yu Mincho),local(YuMincho),local(Source Han Serif JP)," + + "local(Noto Serif JP),local(Noto Serif CJK JP)," + + "local(IPAexMincho),local(IPAMincho),local(Takao Mincho)," + + "local(MS Mincho),local(MS PMincho)", + style: { + style: "normal", + weight: "normal", + }, + }) + ); + expect(fontSubstitution.css).toMatch( + /^"KozMinPr6N",g_d(\d+)_sf(\d+),serif$/ + ); + }); + + it("should substitute HYGoThic-Medium", () => { + const fontName = "HYGoThic-Medium"; + const fontSubstitution = getFontSubstitution( + new Map(), + idFactory, + localFontPath, + fontName, + undefined, + "CIDFontType2" + ); + expect(fontSubstitution).toEqual( + jasmine.objectContaining({ + guessFallback: false, + baseFontName: "HYGoThic-Medium", + src: + "local(Apple SD Gothic Neo),local(AppleGothic)," + + "local(Source Han Sans KR),local(Noto Sans KR)," + + "local(Noto Sans CJK KR),local(Nanum Gothic)," + + "local(Malgun Gothic),local(Dotum),local(Gulim)", + style: { + style: "normal", + weight: "500", + }, + }) + ); + expect(fontSubstitution.css).toMatch( + /^"HYGoThic",g_d(\d+)_sf(\d+),sans-serif$/ + ); + }); }); From 48a12ac225d7a1ab65f48b811eae20654339b15b Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Mon, 25 May 2026 15:03:58 +0200 Subject: [PATCH 3/4] Avoid a temporary variable and return results directly in a couple of functions --- src/core/font_renderer.js | 9 +++------ web/ui_utils.js | 7 ++----- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/core/font_renderer.js b/src/core/font_renderer.js index 0c0fcdd8ebcec..10cbbac0c1cc1 100644 --- a/src/core/font_renderer.js +++ b/src/core/font_renderer.js @@ -37,13 +37,10 @@ function getFloat214(view, offset) { function getSubroutineBias(subrs) { const numSubrs = subrs.length; - let bias = 32768; - if (numSubrs < 1240) { - bias = 107; - } else if (numSubrs < 33900) { - bias = 1131; + if (numSubrs >= 33900) { + return 32768; } - return bias; + return numSubrs < 1240 ? 107 : 1131; } function parseCmap(data, start, end) { diff --git a/web/ui_utils.js b/web/ui_utils.js index e72400ac9d73d..0dd10b39c16e4 100644 --- a/web/ui_utils.js +++ b/web/ui_utils.js @@ -266,14 +266,11 @@ function approximateFraction(x) { b = q; } } - let result; // Select closest of the neighbours to x. if (x_ - a / b < c / d - x_) { - result = x_ === x ? [a, b] : [b, a]; - } else { - result = x_ === x ? [c, d] : [d, c]; + return x_ === x ? [a, b] : [b, a]; } - return result; + return x_ === x ? [c, d] : [d, c]; } /** From 91ca580f31b89190cf48105b2fd506bf2580ea13 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Mon, 25 May 2026 16:50:49 +0200 Subject: [PATCH 4/4] Recover the browser session on test timeout to keep running subsequent tests When a reftest hangs and trips the per-browser timeout, the session was closed, which left every remaining task in the per-browserType queue with no consumer and effectively skipped the rest of the manifest. Instead, mark the in-flight task(s) as failed, reload the page so the driver can reconnect and request the next task, and only fall back to closing the session if the reload itself fails. --- test/test.mjs | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/test/test.mjs b/test/test.mjs index 83aaa5464aeed..6e68c284f4575 100644 --- a/test/test.mjs +++ b/test/test.mjs @@ -423,15 +423,38 @@ async function startRefTest(masterMode, showRefImages) { checkRefsTmp(); } -function handleSessionTimeout(session) { - if (session.closed) { +async function handleSessionTimeout(session) { + if (session.closed || session.recovering) { return; } + const inflightIds = Object.keys(session.tasks); + const suffix = inflightIds.length > 0 ? ` (${inflightIds.join(", ")})` : ""; console.log( - `${TEST_UNEXPECTED_FAIL} | test failed ${session.name} has not responded in ${browserTimeout}s` + `${TEST_UNEXPECTED_FAIL} | test failed ${session.name} has not responded in ${browserTimeout}s${suffix}` ); session.numErrors += session.remaining; session.remaining = 0; + session.taskResults = {}; + session.tasks = {}; + + monitorBrowserTimeout(session, null); + if (session.page) { + session.recovering = true; + try { + await session.page.reload({ + timeout: browserTimeout * 1000, + waitUntil: "domcontentloaded", + }); + session.recovering = false; + monitorBrowserTimeout(session, handleSessionTimeout); + return; + } catch (err) { + console.log( + `Failed to reload ${session.name} after timeout: ${err.message}` + ); + session.recovering = false; + } + } closeSession(session.name); } @@ -768,9 +791,15 @@ async function handleWsBinaryResult(data) { const { browser, id, round, page, failure, lastPageNum, numberOfTasks } = meta; const session = getSession(browser); + if (!session || session.closed) { + return; + } monitorBrowserTimeout(session, handleSessionTimeout); const taskResults = session.taskResults[id]; + if (!taskResults) { + return; + } if (!taskResults[round]) { taskResults[round] = []; }