diff --git a/gulpfile.mjs b/gulpfile.mjs index 01f9d64ed915c..87d9757385b01 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -40,6 +40,7 @@ import { preprocess } from "./external/builder/builder.mjs"; import relative from "metalsmith-html-relative"; import rename from "gulp-rename"; import replace from "gulp-replace"; +import sourcemaps from "gulp-sourcemaps"; import stream from "stream"; import TerserPlugin from "terser-webpack-plugin"; import Vinyl from "vinyl"; @@ -1526,27 +1527,13 @@ gulp.task("types", function (done) { }); function buildLibHelper(bundleDefines, inputStream, outputDir) { - function preprocessLib(content) { - const skipBabel = bundleDefines.SKIP_BABEL; - content = babel.transform(content, { - sourceType: "module", - presets: skipBabel - ? undefined - : [ - [ - "@babel/preset-env", - { ...BABEL_PRESET_ENV_OPTS, loose: false, modules: false }, - ], - ], - plugins: [[babelPluginPDFJSPreprocessor, ctx]], - targets: BABEL_TARGETS, - }).code; - content = content.replaceAll( - /(\sfrom\s".*?)(?:\/src)(\/[^"]*"?;)$/gm, - (all, prefix, suffix) => prefix + suffix - ); - return licenseHeaderLibre + content; - } + const licenseHeader = fs + .readFileSync("./src/license_header.js") + .toString() + .split("\n") + .slice(1, -2) + .map(line => line.replace(/^\s*\*\s?/, "")); + const ctx = { rootPath: __dirname, defines: bundleDefines, @@ -1564,12 +1551,82 @@ function buildLibHelper(bundleDefines, inputStream, outputDir) { "web-null_l10n": "../web/genericl10n.js", }, }; - const licenseHeaderLibre = fs - .readFileSync("./src/license_header_libre.js") - .toString(); - return inputStream - .pipe(transform("utf8", preprocessLib)) - .pipe(gulp.dest(outputDir)); + const enableSourceMaps = bundleDefines.TESTING; + + function preprocessLib(file, _enc, callback) { + const skipBabel = bundleDefines.SKIP_BABEL; + + if (file.isNull()) { + return callback(null, file); + } + + if (file.isStream()) { + return callback(new Error("Streaming not supported")); + } + + try { + // Calculate where the output file will be + const outputFilePath = path.join(__dirname, outputDir, file.relative); + const outputFileDir = path.dirname(outputFilePath); + // Calculate relative path from output directory to source file + const relativeSourcePath = path.relative(outputFileDir, file.path); + + const result = babel.transform(file.contents.toString(), { + sourceType: "module", + presets: skipBabel + ? undefined + : [ + [ + "@babel/preset-env", + { ...BABEL_PRESET_ENV_OPTS, loose: false, modules: false }, + ], + ], + plugins: [ + [babelPluginPDFJSPreprocessor, ctx], + [ + "add-header-comment", + { + header: licenseHeader, + }, + ], + ], + targets: BABEL_TARGETS, + sourceMaps: enableSourceMaps, + sourceFileName: relativeSourcePath, + }); + + let code = result.code; + code = code.replaceAll( + /(\sfrom\s".*?)(?:\/src)(\/[^"]*"?;)$/gm, + (all, prefix, suffix) => prefix + suffix + ); + + file.contents = Buffer.from(code); + // Attach the source map to the file for gulp-sourcemaps + if (result.map) { + file.sourceMap = result.map; + } + + return callback(null, file); + } catch (err) { + return callback(err); + } + } + + let pipeline = inputStream; + if (enableSourceMaps) { + pipeline = pipeline.pipe(sourcemaps.init({ loadMaps: true })); + } + pipeline = pipeline.pipe( + new stream.Transform({ + objectMode: true, + transform: preprocessLib, + }) + ); + if (enableSourceMaps) { + pipeline = pipeline.pipe(sourcemaps.write(".")); + } + return pipeline.pipe(gulp.dest(outputDir)); } function buildLib(defines, dir) { @@ -1956,6 +2013,7 @@ gulp.task( jasmineProcess = spawn("node", options, { stdio: "inherit" }); } else { const options = [ + "--enable-source-maps", "node_modules/jasmine/bin/jasmine", "JASMINE_CONFIG_PATH=test/unit/clitests.json", ]; diff --git a/package-lock.json b/package-lock.json index ab5b52e6ed0d8..4f44e354f008f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@types/node": "^25.2.2", "autoprefixer": "^10.4.24", "babel-loader": "^10.0.0", + "babel-plugin-add-header-comment": "^1.0.3", "c8": "^10.1.3", "cached-iterable": "^0.3.0", "caniuse-lite": "^1.0.30001769", @@ -38,6 +39,7 @@ "gulp-postcss": "^10.0.0", "gulp-rename": "^2.1.0", "gulp-replace": "^1.1.4", + "gulp-sourcemaps": "^3.0.0", "gulp-zip": "^6.1.0", "highlight.js": "^11.11.1", "jasmine": "^5.13.0", @@ -2135,6 +2137,110 @@ "npm": ">=7.0.0" } }, + "node_modules/@gulp-sourcemaps/identity-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz", + "integrity": "sha512-Tb+nSISZku+eQ4X1lAkevcQa+jknn/OVUgZ3XCxEKIsLsqYuPoJwJOPQeaOk75X3WPftb29GWY1eqE7GLsXb1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^6.4.1", + "normalize-path": "^3.0.0", + "postcss": "^7.0.16", + "source-map": "^0.6.0", + "through2": "^3.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@gulp-sourcemaps/identity-map/node_modules/acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/@gulp-sourcemaps/identity-map/node_modules/picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true, + "license": "ISC" + }, + "node_modules/@gulp-sourcemaps/identity-map/node_modules/postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/@gulp-sourcemaps/identity-map/node_modules/through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + }, + "node_modules/@gulp-sourcemaps/map-sources": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz", + "integrity": "sha512-o/EatdaGt8+x2qpb0vFLC/2Gug/xYPRXb6a+ET1wGYKozKN3krDWC/zZFZAtrzxJHuDL12mwdfEFKcKMNvc55A==", + "dev": true, + "license": "MIT", + "dependencies": { + "normalize-path": "^2.0.1", + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@gulp-sourcemaps/map-sources/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@gulp-sourcemaps/map-sources/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/@gulpjs/messages": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@gulpjs/messages/-/messages-1.1.0.tgz", @@ -3826,6 +3932,19 @@ "node": ">= 10.13.0" } }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true, + "license": "(MIT OR Apache-2.0)", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, "node_modules/autoprefixer": { "version": "10.4.24", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", @@ -3911,6 +4030,13 @@ "webpack": ">=5.61.0" } }, + "node_modules/babel-plugin-add-header-comment": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/babel-plugin-add-header-comment/-/babel-plugin-add-header-comment-1.0.3.tgz", + "integrity": "sha512-AYzYg2cGNYfQNPfJU0YHxPxBArEVilJPe3cMJ+g6c3oly2GY1tTfTLaaMqVRd67aiFEPYyHF6dqYjxPHpZ53ow==", + "dev": true, + "license": "MIT" + }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.15", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.15.tgz", @@ -5131,6 +5257,18 @@ "node": ">= 8" } }, + "node_modules/css": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", + "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "source-map": "^0.6.1", + "source-map-resolve": "^0.6.0" + } + }, "node_modules/css-functions-list": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.3.tgz", @@ -5198,6 +5336,20 @@ "node": ">=4" } }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "dev": true, + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", @@ -5280,6 +5432,28 @@ } } }, + "node_modules/debug-fabulous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/debug-fabulous/-/debug-fabulous-1.1.0.tgz", + "integrity": "sha512-GZqvGIgKNlUnHUPQhepnUZFIMoi3dgZKQBzKDeL2g7oJF9SNAji/AAu36dusFUas0O+pae74lNeoIPHqXWDkLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "3.X", + "memoizee": "0.4.X", + "object-assign": "4.X" + } + }, + "node_modules/debug-fabulous/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, "node_modules/decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", @@ -5317,6 +5491,16 @@ "node": ">=0.10.0" } }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -5405,6 +5589,16 @@ "node": ">=4" } }, + "node_modules/detect-newline": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha512-CwffZFvlJffUg9zZA0uqrjQayUTC8ob94pnr5sFwaVv3IOmkfUHcWH+jXaQK3askE51Cqe8/9Ql/0uXNwqZ8Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/devtools-protocol": { "version": "0.0.1566079", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1566079.tgz", @@ -5841,6 +6035,62 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "dev": true, + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -6233,6 +6483,22 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -6311,6 +6577,17 @@ "node": ">=0.10.0" } }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -6417,6 +6694,16 @@ "node": ">=0.10.0" } }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dev": true, + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -7496,6 +7783,60 @@ "node": ">=10" } }, + "node_modules/gulp-sourcemaps": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-3.0.0.tgz", + "integrity": "sha512-RqvUckJkuYqy4VaIH60RMal4ZtG0IbQ6PXMNkNsshEGJ9cldUPRb/YCgboYae+CLAs1HQNb4ADTKCx65HInquQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gulp-sourcemaps/identity-map": "^2.0.1", + "@gulp-sourcemaps/map-sources": "^1.0.0", + "acorn": "^6.4.1", + "convert-source-map": "^1.0.0", + "css": "^3.0.0", + "debug-fabulous": "^1.0.0", + "detect-newline": "^2.0.0", + "graceful-fs": "^4.0.0", + "source-map": "^0.6.0", + "strip-bom-string": "^1.0.0", + "through2": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gulp-sourcemaps/node_modules/acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/gulp-sourcemaps/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/gulp-sourcemaps/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/gulp-zip": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/gulp-zip/-/gulp-zip-6.1.0.tgz", @@ -9201,6 +9542,16 @@ "yallist": "^3.0.2" } }, + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es5-ext": "~0.10.2" + } + }, "node_modules/make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -9326,6 +9677,26 @@ "dev": true, "license": "MIT" }, + "node_modules/memoizee": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "es5-ext": "^0.10.64", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/memory-fs": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", @@ -9652,6 +10023,13 @@ "node": ">= 0.4.0" } }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "dev": true, + "license": "ISC" + }, "node_modules/node-readable-to-web-readable-stream": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/node-readable-to-web-readable-stream/-/node-readable-to-web-readable-stream-0.4.2.tgz", @@ -11861,6 +12239,18 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-resolve": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", + "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "dev": true, + "license": "MIT", + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -12999,6 +13389,20 @@ "node": ">=0.10.0" } }, + "node_modules/timers-ext": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", + "dev": true, + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -13173,6 +13577,13 @@ "summary": "^2.0.0" } }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "dev": true, + "license": "ISC" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -14275,6 +14686,16 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index f03691f45c12f..03ca97ee2ac41 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@types/node": "^25.2.2", "autoprefixer": "^10.4.24", "babel-loader": "^10.0.0", + "babel-plugin-add-header-comment": "^1.0.3", "c8": "^10.1.3", "cached-iterable": "^0.3.0", "caniuse-lite": "^1.0.30001769", @@ -33,6 +34,7 @@ "gulp-postcss": "^10.0.0", "gulp-rename": "^2.1.0", "gulp-replace": "^1.1.4", + "gulp-sourcemaps": "^3.0.0", "gulp-zip": "^6.1.0", "highlight.js": "^11.11.1", "jasmine": "^5.13.0", diff --git a/test/integration/text_layer_spec.mjs b/test/integration/text_layer_spec.mjs index 47799fef4bd3f..f54f24289b5de 100644 --- a/test/integration/text_layer_spec.mjs +++ b/test/integration/text_layer_spec.mjs @@ -432,6 +432,80 @@ describe("Text layer", () => { ); }); }); + + describe("when selecting text with find highlights active", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait("find_all.pdf", ".textLayer", 100); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("doesn't jump when selection anchor is inside a highlight element", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + // Highlight all occurrences of the letter A (case insensitive). + await page.click("#viewFindButton"); + await page.waitForSelector("#findInput", { visible: true }); + await page.type("#findInput", "a"); + await page.click("#findHighlightAll + label"); + await page.waitForSelector(".textLayer .highlight"); + + // find_all.pdf contains 'AB BA' in a monospace font. These are + // the glyph metrics at 100% zoom, extracted from the PDF. + const glyphWidth = 15.98; + const expectedFirstAX = 30; + + // Compute the drag coordinates to select exactly "AB". The + // horizontal positions use the page origin and PDF glyph + // metrics; the vertical center comes from the highlight. + const pageDiv = await page.$(".page canvas"); + const pageBox = await pageDiv.boundingBox(); + const firstHighlight = await page.$(".textLayer .highlight"); + const highlightBox = await firstHighlight.boundingBox(); + + // Drag from beginning of first 'A' to end of second 'B' + const aStart = pageBox.x + expectedFirstAX; + const startY = Math.round( + highlightBox.y + highlightBox.height / 2 + ); + const bEnd = Math.round(aStart + glyphWidth * 2); + + await page.mouse.move(aStart, startY); + await page.mouse.down(); + await moveInSteps( + page, + { x: aStart, y: startY }, + { x: bEnd, y: startY }, + 20 + ); + await page.mouse.up(); + + const selection = await page.evaluate(() => + window.getSelection().toString() + ); + expect(selection).withContext(`In ${browserName}`).toEqual("AB"); + + // The selectionchange handler in TextLayerBuilder walks up + // from .highlight to its parent span before placing + // endOfContent (see text_layer_builder.js). Without that + // fix, endOfContent would be inserted inside the text span + // (as a sibling of the .highlight) instead of as a direct + // child of .textLayer. Verify the correct DOM structure. + const endOfContentIsDirectChild = await page.evaluate(() => { + const eoc = document.querySelector(".textLayer .endOfContent"); + return eoc?.parentElement?.classList.contains("textLayer"); + }); + expect(endOfContentIsDirectChild) + .withContext(`In ${browserName}`) + .toBeTrue(); + }) + ); + }); + }); }); describe("using selection carets", () => { diff --git a/test/integration/thumbnail_view_spec.mjs b/test/integration/thumbnail_view_spec.mjs index 8232ef20d619c..5b35956f6be58 100644 --- a/test/integration/thumbnail_view_spec.mjs +++ b/test/integration/thumbnail_view_spec.mjs @@ -12,6 +12,21 @@ function waitForThumbnailVisible(page, pageNum) { ); } +async function waitForMenu(page, buttonSelector, visible = true) { + return page.waitForFunction( + (selector, vis) => { + const button = document.querySelector(selector); + if (!button) { + return false; + } + return button.getAttribute("aria-expanded") === (vis ? "true" : "false"); + }, + {}, + buttonSelector, + visible + ); +} + describe("PDF Thumbnail View", () => { describe("Works without errors", () => { let pages; @@ -201,4 +216,105 @@ describe("PDF Thumbnail View", () => { ); }); }); + + describe("The manage dropdown menu", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait( + "tracemonkey.pdf", + "#viewsManagerToggleButton", + null, + null, + { enableSplitMerge: true } + ); + }); + + afterEach(async () => { + await closePages(pages); + }); + + async function enableMenuItems(page) { + await page.evaluate(() => { + document + .querySelectorAll("#viewsManagerStatusActionOptions button") + .forEach(button => { + button.disabled = false; + }); + }); + } + + it("should open with Enter key and remain open", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.click("#viewsManagerToggleButton"); + await waitForThumbnailVisible(page, 1); + + await enableMenuItems(page); + + // Focus the manage button + await kbFocusNext(page); + await kbFocusNext(page); + await page.waitForSelector("#viewsManagerStatusActionButton:focus", { + visible: true, + }); + + // Press Enter to open the menu + await page.keyboard.press("Enter"); + + await waitForMenu(page, "#viewsManagerStatusActionButton"); + + // Verify first menu item can be focused + await page.waitForSelector("#viewsManagerStatusActionCopy:focus", { + visible: true, + }); + + // Close menu with Escape + await page.keyboard.press("Escape"); + await waitForMenu(page, "#viewsManagerStatusActionButton", false); + }) + ); + }); + + it("should open with Space key and remain open", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.click("#viewsManagerToggleButton"); + await waitForThumbnailVisible(page, 1); + + await enableMenuItems(page); + + // Focus the manage button + await kbFocusNext(page); + await kbFocusNext(page); + await page.waitForSelector("#viewsManagerStatusActionButton:focus", { + visible: true, + }); + + // Press Space to open the menu + await page.keyboard.press(" "); + + await waitForMenu(page, "#viewsManagerStatusActionButton"); + + // Verify first menu item can be focused + await page.waitForSelector("#viewsManagerStatusActionCopy:focus", { + visible: true, + }); + + // Navigate menu items with arrow keys + await page.keyboard.press("ArrowDown"); + await page.waitForSelector("#viewsManagerStatusActionCut:focus", { + visible: true, + }); + + // Menu should still be open + await waitForMenu(page, "#viewsManagerStatusActionButton"); + + // Close menu with Escape + await page.keyboard.press("Escape"); + await waitForMenu(page, "#viewsManagerStatusActionButton", false); + }) + ); + }); + }); }); diff --git a/web/menu.js b/web/menu.js index 27218093da33b..de917c6ec6689 100644 --- a/web/menu.js +++ b/web/menu.js @@ -70,6 +70,36 @@ class Menu { this.#lastIndex = -1; } + /** + * Open the menu. + */ + #openMenu() { + if (this.#openMenuAC) { + return; + } + + const menu = this.#menu; + this.#triggeringButton.ariaExpanded = "true"; + this.#openMenuAC = new AbortController(); + const signal = AbortSignal.any([ + this.#menuAC.signal, + this.#openMenuAC.signal, + ]); + window.addEventListener( + "pointerdown", + ({ target }) => { + if ( + !this.#triggeringButton.contains(target) && + !menu.contains(target) + ) { + this.#closeMenu(); + } + }, + { signal } + ); + window.addEventListener("blur", this.#closeMenu.bind(this), { signal }); + } + /** * Set up the menu. */ @@ -80,23 +110,7 @@ class Menu { return; } - const menu = this.#menu; - this.#triggeringButton.ariaExpanded = "true"; - this.#openMenuAC = new AbortController(); - const signal = AbortSignal.any([ - this.#menuAC.signal, - this.#openMenuAC.signal, - ]); - window.addEventListener( - "pointerdown", - ({ target }) => { - if (target !== this.#triggeringButton && !menu.contains(target)) { - this.#closeMenu(); - } - }, - { signal } - ); - window.addEventListener("blur", this.#closeMenu.bind(this), { signal }); + this.#openMenu(); }); const { signal } = this.#menuAC; @@ -110,12 +124,10 @@ class Menu { stopEvent(e); break; case "ArrowDown": - case "Tab": this.#goToNextItem(e.target, true); stopEvent(e); break; case "ArrowUp": - case "ShiftTab": this.#goToNextItem(e.target, false); stopEvent(e); break; @@ -124,7 +136,7 @@ class Menu { .find( item => !item.disabled && !item.classList.contains("hidden") ) - .focus(); + ?.focus(); stopEvent(e); break; case "End": @@ -132,7 +144,7 @@ class Menu { .findLast( item => !item.disabled && !item.classList.contains("hidden") ) - .focus(); + ?.focus(); stopEvent(e); break; default: @@ -159,27 +171,27 @@ class Menu { case "Enter": case "ArrowDown": case "Home": + stopEvent(e); if (!this.#openMenuAC) { - this.#triggeringButton.click(); + this.#openMenu(); } this.#menuItems .find( item => !item.disabled && !item.classList.contains("hidden") ) - .focus(); - stopEvent(e); + ?.focus(); break; case "ArrowUp": case "End": + stopEvent(e); if (!this.#openMenuAC) { - this.#triggeringButton.click(); + this.#openMenu(); } this.#menuItems .findLast( item => !item.disabled && !item.classList.contains("hidden") ) - .focus(); - stopEvent(e); + ?.focus(); break; case "Escape": this.#closeMenu(); diff --git a/web/text_layer_builder.js b/web/text_layer_builder.js index 184c244a3d90a..8fe0110abb6be 100644 --- a/web/text_layer_builder.js +++ b/web/text_layer_builder.js @@ -307,6 +307,9 @@ class TextLayerBuilder { if (anchor.nodeType === Node.TEXT_NODE) { anchor = anchor.parentNode; } + if (anchor.classList?.contains("highlight")) { + anchor = anchor.parentNode; + } if (!modifyStart && range.endOffset === 0) { do { while (!anchor.previousSibling) { diff --git a/web/viewer.html b/web/viewer.html index 42b2345e7cc5e..bd521d6487f5c 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -207,22 +207,22 @@
  • -
  • -
  • -
  • -