Compare commits

...

60 Commits

Author SHA1 Message Date
Jonas Jenwald
4806d8294e
Merge pull request #21695 from martinthomson/open-instructions
Document how to use the viewer to open files
2026-08-04 12:18:00 +02:00
Jonas Jenwald
72a76e585b
Merge pull request #21690 from Snuffleupagus/Menu-#goToInitial
Add a go to first/last menu-item helper method in the `Menu` class
2026-08-04 10:20:46 +02:00
Martin Thomson
ca44b47751 Document how to use the viewer to open files
This is not an option that appears in the Firefox integration,
so it is not obvious how to find it.
2026-08-04 09:40:34 +10:00
Jonas Jenwald
fc610d3e59 Add a go to first/last menu-item helper method in the Menu class
This fixes a bug when using the <kbd>Home</kbd> and <kbd>End</kbd> keyboard shortcuts to navigate through a `Menu` instance. These two buttons didn't update the `#lastIndex` field, which means that e.g. a following <kbd>ArrowDown</kbd> or <kbd>ArrowUp</kbd> press could make focus "jump" to an unexpected menu-item.

Also, the helper method reduces a little bit of code duplication in the event handlers.
2026-08-03 22:25:52 +02:00
calixteman
b4ba666b0c
Merge pull request #21694 from calixteman/improve_font_familly_san
Safely serialize CSS font family names
2026-08-03 21:12:10 +02:00
calixteman
4c4e8feafd
Merge pull request #21693 from calixteman/fix/quad-regex-autolinker
Bound the email parts in the autolinker regex
2026-08-03 18:03:43 +02:00
calixteman
f21fe34747
Safely serialize CSS font family names 2026-08-03 18:02:10 +02:00
calixteman
ba7bf7b26c
Merge pull request #21683 from calixteman/fix/quad-regex-writer
Don't write numbers in exponential notation when saving a pdf
2026-08-03 17:49:23 +02:00
calixteman
1da36fb6bf
Merge pull request #21686 from calixteman/fix/quad-regex-xfa-path
Anchor the regex used to extract the XFA path positions
2026-08-03 17:46:24 +02:00
calixteman
92d027eaeb
Anchor the regex used to extract the XFA path positions
Matching the name with a leading `.+` is quadratic in the length of
a component which doesn't end with a position, and every AcroForm
field name goes through this.
2026-08-03 17:07:48 +02:00
calixteman
5375bff642
Don't write numbers in exponential notation when saving a pdf
`toFixed(10)` switches to the exponential notation from 1e21 on, which isn't
valid PDF syntax, and removing the trailing zeros then dropped a digit of the
exponent: 1e30 was written "1e+3" and 1e100 "1e+1". Such a number, necessarily
an integer, is now written with all its digits.

The trailing zeros are removed with a backward scan, since `toFixed(10)` always
produces exactly 10 decimals. Below the 1e21 limit its output is at most 33
characters long, so the previous `$`-anchored regex wasn't a performance issue.
2026-08-03 16:59:12 +02:00
calixteman
b1f51818ae
Bound the email parts in the autolinker regex
The local part and the domain labels were unbounded, making the
search quadratic in the length of a run of characters preceding
an "@": scanning the text of a single page could take seconds.
2026-08-03 16:38:02 +02:00
Tim van der Meij
ae976b924b
Merge pull request #21671 from Snuffleupagus/getFieldObjects-Map
[api-minor] Convert `getFieldObjects` to return data in a Map
2026-08-02 22:37:22 +02:00
Tim van der Meij
ec691130e6
Merge pull request #21692 from calixteman/fix/quad-regex-xml-entities
Exclude "&" from the XML entity names
2026-08-02 22:24:11 +02:00
Jonas Jenwald
88716313d6 Bump library version to 6.3
See commit ce4ff55faaa83b39b0137dc458af6eea6f96235f
2026-08-02 21:27:09 +02:00
Jonas Jenwald
82624a5e50 [api-minor] Convert getFieldObjects to return data in a Map
Compared to regular Objects there's a number of advantages to using Maps:
 - They support proper iteration.
 - They have a simple way to check for the existence of data.
 - They have a simple/efficient way to check the number of elements.

If this functionality was added today, I cannot imagine that we'd choose an Object for this data.

In the Firefox PDF Viewer sending Maps to the scripting-implementation should be fine, since it uses the browser `Cu.cloneInto` functionality; see https://searchfox.org/firefox-main/source/toolkit/components/pdfjs/content/PdfSandbox.sys.mjs
However with QuickJS, used by the GENERIC viewer, all data needs to be stringified and Maps are converted into regular Objects (see also PR 21664). Hence the `objects` property, in the scripting-implementation, is converted back into a Map using the (renamed) `createMap` helper function.
2026-08-02 21:27:07 +02:00
Jonas Jenwald
7590ad5312
Merge pull request #21689 from timvandermeij/updates
Update dependencies to the most recent versions
2026-08-02 21:25:38 +02:00
Jonas Jenwald
923d48ead4
Merge pull request #21691 from Snuffleupagus/Field-setAction-Map
Add scripts correctly in `Field.prototype.setAction` (PR 12569 follow-up)
2026-08-02 21:24:24 +02:00
Jonas Jenwald
1f9fbc764e Add scripts correctly in Field.prototype.setAction (PR 12569 follow-up)
In PR 12569 the `_actions` class-field was changed from an Object into a Map, with *most* of the code updated to reflect that.
However, in the `setAction` method it's still treated as an Object which means that any added script will simply be ignored. Most likely that part of the scripting-implementation isn't being used, since this code has been "wrong" for close to six years now.
2026-08-02 20:31:15 +02:00
calixteman
f7f30dd844
Exclude "&" from the XML entity names
Scanning to the end of the string for every "&" made the entity
resolution quadratic. Stopping at the next "&" also fixes a bare
ampersand swallowing the reference which follows it.
2026-08-02 18:59:19 +02:00
Tim van der Meij
d2ebbd10de
Fix vulnerability in the brace-expansion dependency
This patch is automatically generated with `npm audit fix` and fixes
CVE-2026-14257.
2026-08-02 13:23:04 +02:00
Tim van der Meij
09ad726a3f
Update dependencies to the most recent versions 2026-08-02 13:22:33 +02:00
Tim van der Meij
d0779c411e
Merge pull request #21687 from calixteman/fix/quad-regex-delete-word
Scan backwards to delete a word in a text field
2026-08-02 13:04:00 +02:00
Tim van der Meij
89852881b8
Merge pull request #21685 from calixteman/fix/quad-regex-pdf-filename
Find the PDF filename in a URL hash in two linear steps
2026-08-02 12:57:49 +02:00
Tim van der Meij
eb8f6af8cc
Merge pull request #21684 from calixteman/fix/quad-regex-headers
Trim the response headers with a backward scan
2026-08-02 12:46:03 +02:00
Tim van der Meij
f73978083d
Merge pull request #21680 from calixteman/fix/quad-regex
Fix the regex used to normalize css fonts in XFA
2026-08-02 12:43:14 +02:00
Tim van der Meij
5dc1d0c5d5
Merge pull request #21682 from calixteman/fix/xml-invalid-char-ref
Don't throw on an invalid XML character reference
2026-08-02 12:26:49 +02:00
Tim van der Meij
2475c4ec91
Merge pull request #21678 from Snuffleupagus/ViewHistory-findIndex
Shorten the `ViewHistory` class a little bit
2026-08-02 12:10:21 +02:00
calixteman
08f6769390
Scan backwards to delete a word in a text field
Finding the word to delete with a regex is quadratic in the value
length, so each "delete word backward" keystroke could take a long
time in a large field.
2026-08-01 22:13:27 +02:00
calixteman
7862875438
Find the PDF filename in a URL hash in two linear steps
Searching for a name followed by ".pdf" is quadratic on a hash which
doesn't contain one, so locate the last ".pdf" first and then extend
it to the left.
2026-08-01 21:17:43 +02:00
calixteman
cba911df86
Trim the response headers with a backward scan
Removing the trailing whitespace with a `$`-anchored regex is
quadratic in the length of the run, which a server controls.
The helper lives in network_utils.js, to be unit testable.
2026-08-01 20:56:01 +02:00
calixteman
cec8d2eed6
Don't throw on an invalid XML character reference
`String.fromCodePoint` throws on anything which isn't a code point,
so e.g. "&#xZZ;" or "&#x110000;" aborted the whole parsing. Such a
reference is now kept as-is, like an unknown named entity.
2026-08-01 16:24:03 +02:00
calixteman
491792f0e2
Fix the regex used to normalize css fonts in XFA
The regex was quadratic in the number of consecutive spaces,
which caused performance issues when normalizing fonts with
a large number of spaces.
2026-08-01 15:11:43 +02:00
Jonas Jenwald
664526ee65 Shorten the ViewHistory class a little bit
- Replace the manual loop, used to find an existing entry, with the native [`findIndex` method](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex).

 - Remove the `fingerprint`/`cacheSize` class fields, since they are completely unused.
2026-08-01 12:19:20 +02:00
Tim van der Meij
7fc7072f9c
Merge pull request #21674 from calixteman/touch/fix-swallowed-pointerup
Let pointerup/cancel propagate in TouchManager
2026-07-31 20:16:07 +02:00
Tim van der Meij
bb1a948397
Merge pull request #21672 from Snuffleupagus/more-ternary
Replace simple return `if` statements with ternary operators
2026-07-31 19:59:46 +02:00
Tim van der Meij
4b4c784a85
Merge pull request #21673 from Snuffleupagus/EventBus-Map-Set
Re-factor the `EventBus` to use Map/Set internally
2026-07-31 19:50:16 +02:00
calixteman
0f999e27b5 Let pointerup/cancel propagate in TouchManager
During two-finger gestures, keep preventing the default action on pointerup/pointercancel, but don't stop propagation. Resize/drag sessions need those bubble events to clean up.
2026-07-31 19:26:53 +02:00
calixteman
af38789499
Merge pull request #21659 from calixteman/fix/pinch-zoom-origin
Use the client coordinates for the pinch-zoom origin
2026-07-31 18:00:52 +02:00
Jonas Jenwald
0d5df9d96e Re-factor the EventBus to use Map/Set internally
Especially the Array to Set conversion should be helpful, since adding/removing elements from a Set is more efficient.
2026-07-31 17:37:05 +02:00
Calixte Denizet
8960eddfc1 Use the client coordinates for the pinch-zoom origin
`TouchManager` computes the pinch distances with the screen coordinates, but it
was passing the pinch center in that space too, while the viewer expects a
client coordinate, like the origin coming from a wheel event.

Since a two-finger gesture can be synthesized with WebDriver BiDi, the
pinch-zoom test doesn't have to rely on CDP anymore and can run in Chrome, where
it fails without the above fix.
2026-07-31 16:44:28 +02:00
Jonas Jenwald
ee59db5018 Replace simple return if statements with ternary operators
In a couple of cases, where a boolean is returned, it's also possible to use an OR operator rather a ternary.

*Note:* This patch reduces the size of the `gulp mozcentral` bundle by `960` bytes.
2026-07-31 12:24:08 +02:00
Jonas Jenwald
ce4ff55faa
Merge pull request #21664 from Snuffleupagus/getJSActions-api
[api-minor] Convert `getJSActions` to return data in a Map
2026-07-31 11:29:21 +02:00
calixteman
40f6492997
Merge pull request #21665 from calixteman/sync_firefox_css
Sync the viewer chrome with the Firefox design system
2026-07-31 09:48:01 +02:00
calixteman
613e004b2e
Merge pull request #21669 from mozilla/update-locales
l10n: Update locale files
2026-07-31 09:19:53 +02:00
github-actions[bot]
734b21e61e l10n: Update locale files 2026-07-31 00:39:04 +00:00
Calixte Denizet
b4d028132b Sync the viewer chrome with the Firefox design system
The chrome is now authored against Firefox design-token names with the shipped
value as a fallback `var(--fx-token, <literal>)` so colors, borders and radii
follow Firefox instead of having to be re-synced by hand.

In MOZCENTRAL viewer.css imports chrome://global/skin/design-system/tokens-brand.css,
gated on the new pdfjs.enableNova pref (default false, read straight from CSS via -moz-pref()).
With the pref off the sheet isn't applied, every fallback resolves, and the
viewer renders as before, as it also does in the GENERIC and components builds,
which never see the chrome sheet.

Consuming a token means never declaring its name in an unlayered rule, which
would shadow Firefox's for the whole subtree; --focus-outline, --border-color and
--text-color/--button-border-color are therefore renamed to
--editor-selection-outline, --signature-border-color and --views-*. In forced
colors each state reads a matching background/foreground/border triple so it
can't collapse to a single system color.

The dialog button styling also moves out of `.dialog .mainContainer button` into
a shared .primaryButton/.secondaryButton primitive (buttons.css, imported from
pdf_viewer.css so the components build gets it too). The password,
document-properties, print and undo-bar buttons use it in place of .dialogButton,
and the custom radio appearance gives way to native controls tinted with
accent-color.
2026-07-30 23:13:43 +02:00
Tim van der Meij
a80897dc9a
Merge pull request #21666 from calixteman/bug2054348
Fix the selection rendering when a page has been destroyed and rendered again (bug 2054348)
2026-07-30 21:12:47 +02:00
Tim van der Meij
0163438e1a
Merge pull request #21660 from calixteman/fix/pdf-editor-preserve-widget-parent
Avoid mutating source widget parents
2026-07-30 21:02:29 +02:00
Tim van der Meij
f70f55d743
Merge pull request #21658 from calixteman/fix/pdf-editor-clone-reference-race
Avoid duplicate shared object clones
2026-07-30 20:52:35 +02:00
Calixte Denizet
9e02ceed09 Fix the selection rendering when a page has been destroyed and rendered again (bug 2054348)
The draw layer keeps a reference on the text layer div in order to render the
selection, but it was destroyed only along with the annotation editor layer.
Hence, when the editor is disabled, like in Firefox for Android, scrolling far
enough to destroy a page view left the draw layer with a reference on a removed
text layer: the new one was never registered, consequently no selection was
rendered anymore on that page.
2026-07-30 18:37:03 +02:00
Jonas Jenwald
803c9d7d21 [api-minor] Convert getJSActions to return data in a Map
Compared to regular Objects there's a number of advantages to using Maps:
 - They support proper iteration.
 - They have a simple way to check for the existence of data.
 - They have a simple/efficient way to check the number of elements.

If this functionality was added today, I cannot imagine that we'd choose an Object for this data.

Note also how in the scripting-implementation the `actions` were already converted into a Map, via the `createActionsMap` helper.
In the Firefox PDF Viewer sending `Map`s to the scripting-implementation should be fine, since it uses the browser `Cu.cloneInto` functionality; see https://searchfox.org/firefox-main/source/toolkit/components/pdfjs/content/PdfSandbox.sys.mjs
However with QuickJS, used by the GENERIC viewer, all data needs to be stringified and unfortunately `JSON.stringify()` doesn't support Maps. Hence we convert Maps to Objects, via a [`replacer` function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#replacer), since the existing `createActionsMap` usage will convert the actions-Objects back to Maps.
2026-07-30 16:04:58 +02:00
Jonas Jenwald
73707b62e9
Merge pull request #21661 from Snuffleupagus/more-optional-chaining-6
Use more more optional chaining in the `src/` folder
2026-07-30 10:35:17 +02:00
Jonas Jenwald
1813ab0774 Use more more optional chaining in the src/ folder 2026-07-29 18:28:41 +02:00
Calixte Denizet
aa812b531a Avoid mutating source widget parents 2026-07-29 16:03:15 +02:00
calixteman
61acf93172
Merge pull request #21644 from calixteman/editor/pinch-lost-after-undo
[Editor] Restore pinch-to-resize on an editor which came back from an undo
2026-07-29 15:27:54 +02:00
calixteman
822d3ea809 [Editor] Restore pinch-to-resize on an editor which came back from an undo
`AnnotationEditor.remove()` destroys the editor's `TouchManager`, but `remove()`
is also the undo half of an edit: deleting a drawing and undoing it, or undoing
the drawing itself and redoing it, brings the very same editor back.
2026-07-29 14:45:02 +02:00
Calixte Denizet
3a19a20d50 Avoid duplicate shared object clones 2026-07-28 22:39:35 +02:00
Tim van der Meij
4d9007e6f9
Merge pull request #21655 from timvandermeij/bump
Bump the stable version in `pdfjs.config`
2026-07-28 21:59:15 +02:00
Tim van der Meij
7f5fd4eba4
Bump the stable version in pdfjs.config 2026-07-28 21:53:58 +02:00
100 changed files with 2732 additions and 1398 deletions

View File

@ -30,6 +30,9 @@ latest JavaScript features; please also see [this wiki page](https://github.com/
+ Older browsers: https://mozilla.github.io/pdf.js/legacy/web/viewer.html
> [!NOTE]
> Open new files via the menu (the ">>" icon) or by dragging and dropping.
### Browser Extensions
#### Firefox

View File

@ -73,10 +73,7 @@ function preprocess(inFilename, outFilename, defines) {
const out = [];
let i = 0;
function readLine() {
if (i < totalLines) {
return lines[i++];
}
return null;
return i < totalLines ? lines[i++] : null;
}
const writeLine =
typeof outFilename === "function"
@ -127,10 +124,7 @@ function preprocess(inFilename, outFilename, defines) {
function expand(line) {
line = line.replaceAll(/__\w+__/g, function (variable) {
variable = variable.substring(2, variable.length - 2);
if (variable in defines) {
return defines[variable];
}
return "";
return variable in defines ? defines[variable] : "";
});
writeLine(line);
}

View File

@ -52,21 +52,61 @@ pdfjs-bookmark-button-label = بلگه هیم سکویی
pdfjs-tools-button =
.title = ٱوزارا
pdfjs-tools-button-label = ٱوزارا
pdfjs-first-page-button =
.title = رئڌن و بلگه نیایی
pdfjs-first-page-button-label = رئڌن و بلگه نیایی
pdfjs-last-page-button =
.title = رئڌن و بلگه دیندایی
pdfjs-last-page-button-label = رئڌن و بلگه دیندایی
pdfjs-page-rotate-cw-button =
.title = لر خردن ساعتگرد
pdfjs-page-rotate-cw-button-label = لر خردن ساعتگرد
pdfjs-page-rotate-ccw-button =
.title = لر خردن پاد ساعتگرد
pdfjs-page-rotate-ccw-button-label = لر خردن پاد ساعتگرد
pdfjs-cursor-text-select-tool-button =
.title = فعال کردن ٱوزار پسند هؽل
pdfjs-cursor-text-select-tool-button-label = ٱوزار پسند هؽل
pdfjs-cursor-hand-tool-button =
.title = فعال کردن ٱوزار دست
pdfjs-cursor-hand-tool-button-label = ٱوزار دست
pdfjs-scroll-page-button =
.title = و کار گرؽڌن اسکرۊل بلگه
pdfjs-scroll-page-button-label = اسکرۊل بلگه
pdfjs-scroll-vertical-button =
.title = و کار گرؽڌن اسکرۊل عمۊدی
pdfjs-scroll-vertical-button-label = اسکرۊل عمۊدی
pdfjs-scroll-horizontal-button =
.title = و کار گرؽڌن اسکرۊل اوفوقی
pdfjs-scroll-horizontal-button-label = اسکرۊل اوفوقی
pdfjs-scroll-wrapped-button =
.title = و کار گرؽڌن اسکرۊل پؽچسته
pdfjs-scroll-wrapped-button-label = اسکرۊل پؽچسته
## Document properties dialog
pdfjs-document-properties-button =
.title = خۊسۊسیات سند…
pdfjs-document-properties-button-label = خۊسۊسیات سند…
pdfjs-document-properties-file-name = نوم فایل:
pdfjs-document-properties-file-size = هندا فایل:
pdfjs-document-properties-title = عونوان:
pdfjs-document-properties-author = هؽل کوݩ:
pdfjs-document-properties-subject = سرتال:
pdfjs-document-properties-creation-date = تاریخ وورکل وابیڌن:
pdfjs-document-properties-modification-date = تاریخ آلشتکاری:
# Variables:
# $dateObj (Date) - the creation/modification date and time of the PDF file
pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") }
pdfjs-document-properties-creator = وورکل کون:
pdfjs-document-properties-producer = وورکل کون PDF:
pdfjs-document-properties-version = نوسخه PDF:
pdfjs-document-properties-page-count = تئداد بلگه یل:
pdfjs-document-properties-page-size = هندا بلگه:
pdfjs-document-properties-page-size-unit-inches = اینچ
pdfjs-document-properties-page-size-unit-millimeters = میلی متر
pdfjs-document-properties-page-size-orientation-portrait = portrait
pdfjs-document-properties-page-size-orientation-landscape = landscape
pdfjs-document-properties-page-size-name-a-three = A3
pdfjs-document-properties-page-size-name-a-four = A4
pdfjs-document-properties-page-size-name-letter = نامه
@ -84,12 +124,16 @@ pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $hei
##
# The linearization status of the document; usually called "Fast Web View" in
# English locales of Adobe software.
pdfjs-document-properties-linearized = نیشتن زل وب:
pdfjs-document-properties-linearized-yes = هری
pdfjs-document-properties-linearized-no = ن
pdfjs-document-properties-close-button = بستن
## Print
pdfjs-print-progress-message = ٱماڌه کردن سند سی چاپ کردن…
# Variables:
# $progress (Number) - percent value
pdfjs-print-progress-percent = { $progress }%
@ -99,6 +143,55 @@ pdfjs-print-progress-close-button = لقو
pdfjs-toggle-sidebar-button =
.title = آلشت هالت نوار کلی
pdfjs-toggle-sidebar-button-label = آلشت هالت نوار کلی
pdfjs-document-outline-button-label = تئر سند
pdfjs-attachments-button =
.title = نشووݩ داڌن پیوستا
pdfjs-attachments-button-label = پیوستا
pdfjs-layers-button-label = لایه یل
pdfjs-thumbs-button =
.title = نشووݩ داڌن شؽواتا کۊچیر
pdfjs-thumbs-button-label = شؽواتا کۊچیر
pdfjs-findbar-button =
.title = جوستن من سند
pdfjs-findbar-button-label = جوستن
pdfjs-additional-layers = لایه یل ازافه
## Thumbnails panel item (tooltip and alt text for images)
# Variables:
# $page (Number) - the page number
pdfjs-thumb-page-title =
.title = بلگه { $page }
## Find panel button title and messages
pdfjs-find-previous-button-label = دیندایی
pdfjs-find-next-button-label = بئڌی
pdfjs-find-highlight-checkbox = هایلایت کردن پوی
## Predefined zoom values
pdfjs-page-scale-width = پئنا بلگه
pdfjs-page-scale-fit = هندا کردن بلگه
pdfjs-page-scale-auto = زۊم کردن خوتکار
pdfjs-page-scale-actual = هندا واقعی‌
# Variables:
# $scale (Number) - percent value for page scale
pdfjs-page-scale-percent = { $scale }%
## PDF page
# Variables:
# $page (Number) - the page number
pdfjs-page-landmark =
.aria-label = بلگه { $page }
## Annotations
# Variables:
# $dateObj (Date) - the modification date and time of the annotation
pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") }
## Password
@ -107,6 +200,12 @@ pdfjs-password-cancel-button = لقو
## Editing
pdfjs-editor-free-text-button =
.title = هؽل
pdfjs-editor-free-text-button-label = هؽل
pdfjs-editor-ink-button =
.title = کشیڌن
pdfjs-editor-ink-button-label = کشیڌن
pdfjs-editor-stamp-button =
.title = ٱووردن یا آلشت شؽواتا
pdfjs-editor-stamp-button-label = ٱووردن یا آلشت شؽواتا
@ -118,6 +217,14 @@ pdfjs-editor-stamp-editor =
##
# Editor Parameters
pdfjs-editor-free-text-color-input = رنگ
pdfjs-editor-free-text-size-input = هندا
pdfjs-editor-ink-color-input = رنگ
pdfjs-editor-ink-thickness-input = کۊلۊفتی
pdfjs-editor-ink-opacity-input = کر بیڌن
# This refers to the thickness of the line used for free highlighting (not bound to text)
pdfjs-editor-free-highlight-thickness-input = کۊلۊفتی
# .default-content is used as a placeholder in an empty text editor.
pdfjs-free-text2 =
.aria-label = آلشتگر هؽل
@ -127,15 +234,55 @@ pdfjs-editor-comments-sidebar-no-comments-link = قلوه دووسته بۊین
## Alt-text dialog
pdfjs-editor-alt-text-cancel-button = لقو
pdfjs-editor-alt-text-save-button = زفت
## Color picker
pdfjs-editor-colorpicker-yellow =
.title = هیل
pdfjs-editor-colorpicker-green =
.title = ساوز
pdfjs-editor-colorpicker-blue =
.title = کوۊ
pdfjs-editor-colorpicker-pink =
.title = آل
pdfjs-editor-colorpicker-red =
.title = سوئر
## Show all highlights
## This is a toggle button to show/hide all the highlights.
pdfjs-editor-highlight-show-all-button-label = نشووݩ داڌن پوی
pdfjs-editor-highlight-show-all-button =
.title = نشووݩ داڌن پوی
## New alt-text dialog
## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy.
pdfjs-editor-new-alt-text-disclaimer-learn-more-url = قلوه دووسته بۊین
pdfjs-editor-new-alt-text-not-now-button = سکو ن
pdfjs-editor-new-alt-text-error-close-button = بستن
## "Annotations removed" bar
pdfjs-editor-undo-bar-undo-button-label = وورگندن
pdfjs-editor-undo-bar-close-button =
.title = بستن
pdfjs-editor-undo-bar-close-button-label = بستن
## Tab panels
pdfjs-editor-add-signature-draw-thickness-range-label = کۊلۊفتی
## Controls
pdfjs-editor-add-signature-error-close-button = بستن
## Dialog buttons
pdfjs-editor-add-signature-cancel-button = لقو
pdfjs-editor-add-signature-add-button = ٱووردن
pdfjs-editor-edit-signature-update-button = ورۊ رسۊوی
## Comment popup
@ -147,4 +294,32 @@ pdfjs-editor-edit-comment-popup-button =
# An existing comment is edited
pdfjs-editor-edit-comment-dialog-title-when-editing = آلشت منشڌ
pdfjs-editor-edit-comment-dialog-save-button-when-editing = ورۊ رسۊوی
pdfjs-editor-edit-comment-dialog-save-button-when-adding = ٱووردن
pdfjs-editor-edit-comment-dialog-text-input =
.placeholder = ناهاڌن پا هؽل کردن…
pdfjs-editor-edit-comment-dialog-cancel-button = لقو
## The view manager is a sidebar displaying different views:
## - thumbnails;
## - outline;
## - attachments;
## - layers.
## The thumbnails view is used to edit the pdf: remove/insert pages, ...
pdfjs-views-manager-sidebar =
.aria-label = نوار کلی
pdfjs-views-manager-layers-option-label = لایه یل
pdfjs-views-manager-pages-status-action-button-label = دؽوۉداری
pdfjs-views-manager-pages-status-copy-button-label = لف گیری
pdfjs-views-manager-pages-status-cut-button-label = بۊریڌن
pdfjs-views-manager-pages-status-delete-button-label = پاک کردن
pdfjs-views-manager-status-undo-button-label = وورگندن
pdfjs-views-manager-status-done-button-label = ٱنجوم وابی
pdfjs-views-manager-status-close-button =
.title = بستن
pdfjs-views-manager-status-close-button-label = بستن
pdfjs-views-manager-paste-button-label = جا وندن
# Badge used to promote a new feature in the UI, keep it as short as possible.
# It's spelled uppercase for English, but it can be translated as usual.
pdfjs-new-badge-content = نۊ

View File

@ -67,8 +67,8 @@ pdfjs-page-rotate-cw-button =
.title = Im Uhrzeigersinn drehen
pdfjs-page-rotate-cw-button-label = Im Uhrzeigersinn drehen
pdfjs-page-rotate-ccw-button =
.title = Gegen Uhrzeigersinn drehen
pdfjs-page-rotate-ccw-button-label = Gegen Uhrzeigersinn drehen
.title = Gegen den Uhrzeigersinn drehen
pdfjs-page-rotate-ccw-button-label = Gegen den Uhrzeigersinn drehen
pdfjs-cursor-text-select-tool-button =
.title = Textauswahl-Werkzeug aktivieren
pdfjs-cursor-text-select-tool-button-label = Textauswahl-Werkzeug
@ -299,7 +299,7 @@ pdfjs-rendering-error = Beim Darstellen der Seite trat ein Fehler auf.
# (32000-1:2008 Table 169 Annotation types).
# Some common types are e.g.: "Check", "Text", "Comment", "Note"
pdfjs-text-annotation-type =
.alt = [Anlage: { $type }]
.alt = [{ $type } Anmerkung]
# Variables:
# $dateObj (Date) - the modification date and time of the annotation
pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") }
@ -626,7 +626,7 @@ pdfjs-editor-add-signature-save-checkbox = Unterschrift speichern
pdfjs-editor-add-signature-save-warning-message = Sie haben die Grenze von 5 gespeicherten Unterschriften erreicht. Entfernen Sie eine, um weitere zu speichern.
pdfjs-editor-add-signature-image-upload-error-title = Grafik konnte nicht hochgeladen werden
pdfjs-editor-add-signature-image-upload-error-description = Überprüfen Sie Ihre Netzwerkverbindung, oder versuchen Sie es mit einer anderen Grafik.
pdfjs-editor-add-signature-image-no-data-error-title = Kann Grafik nicht in eine Signatur umwandeln
pdfjs-editor-add-signature-image-no-data-error-title = Kann Grafik nicht in eine Unterschrift umwandeln
pdfjs-editor-add-signature-image-no-data-error-description = Bitte versuchen Sie, eine andere Grafik hochzuladen.
pdfjs-editor-add-signature-error-close-button = Schließen
@ -730,9 +730,9 @@ pdfjs-views-manager-pages-status-undo-delete-label =
}
pdfjs-views-manager-pages-status-waiting-ready-label = Ihre Datei wird vorbereitet…
pdfjs-views-manager-pages-status-waiting-uploading-label = Datei wird hochgeladen…
pdfjs-views-manager-status-warning-cut-label = Ausschneiden war nicht möglich. Aktualisieren Sie die Seite und versuchen Sie es erneut.
pdfjs-views-manager-status-warning-cut-label = Ausschneiden nicht möglich. Aktualisieren Sie die Seite und versuchen Sie es erneut.
pdfjs-views-manager-status-warning-copy-label = Kopieren nicht möglich. Aktualisieren Sie die Seite und versuchen Sie es erneut.
pdfjs-views-manager-status-warning-delete-label = Löschen war nicht möglich. Aktualisieren Sie die Seite und versuchen Sie es erneut.
pdfjs-views-manager-status-warning-delete-label = Löschen nicht möglich. Aktualisieren Sie die Seite und versuchen Sie es erneut.
pdfjs-views-manager-status-warning-save-label = Speichern nicht möglich. Aktualisieren Sie die Seite und versuchen Sie es erneut.
pdfjs-views-manager-status-undo-button-label = Rückgängig
pdfjs-views-manager-status-done-button-label = Fertig
@ -824,8 +824,8 @@ pdfjs-digital-signature-properties-certificate-revoked = Zertifikat: Widerrufen
## Main menu for adding/removing signatures
pdfjs-editor-delete-signature-button1 =
.title = Gespeicherte Signatur entfernen
pdfjs-editor-delete-signature-button-label1 = Gespeicherte Signatur entfernen
.title = Gespeicherte Unterschrift entfernen
pdfjs-editor-delete-signature-button-label1 = Gespeicherte Unterschrift entfernen
## Editor toolbar

View File

@ -162,8 +162,8 @@ pdfjs-print-progress-message = Preparando documento para impresión…
# $progress (Number) - percent value
pdfjs-print-progress-percent = { $progress }%
pdfjs-print-progress-close-button = Cancelar
pdfjs-printing-not-supported = Advertencia: La impresión no esta completamente soportada por este navegador.
pdfjs-printing-not-ready = Advertencia: El PDF no cargo completamente para impresión.
pdfjs-printing-not-supported = Advertencia: La impresión no está completamente soportada por este navegador.
pdfjs-printing-not-ready = Advertencia: El PDF no cargó completamente para impresión.
## Tooltips and alt text for side panel toolbar buttons
@ -266,7 +266,7 @@ pdfjs-page-landmark =
## Loading indicator messages
pdfjs-loading-error = Un error ocurrió al cargar el PDF.
pdfjs-invalid-file-error = Archivo PDF invalido o dañado.
pdfjs-invalid-file-error = Archivo PDF inválido o dañado.
pdfjs-missing-file-error = Archivo PDF no encontrado.
pdfjs-unexpected-response-error = Respuesta inesperada del servidor.
pdfjs-rendering-error = Un error ocurrió al renderizar la página.
@ -361,7 +361,7 @@ pdfjs-editor-remove-signature-button =
pdfjs-editor-free-text-color-input = Color
pdfjs-editor-free-text-size-input = Tamaño
pdfjs-editor-ink-color-input = Color
pdfjs-editor-ink-thickness-input = Grossor
pdfjs-editor-ink-thickness-input = Grosor
pdfjs-editor-ink-opacity-input = Opacidad
pdfjs-editor-stamp-add-image-button =
.title = Agregar imagen
@ -581,7 +581,7 @@ pdfjs-editor-add-signature-type-input =
.aria-label = Escribe tu firma
.placeholder = Escribe tu firma
pdfjs-editor-add-signature-draw-placeholder = Dibuja tu firma
pdfjs-editor-add-signature-draw-thickness-range-label = Grossor
pdfjs-editor-add-signature-draw-thickness-range-label = Grosor
# Variables:
# $thickness (Number) - the thickness (in pixels) of the line used to draw a signature.
pdfjs-editor-add-signature-draw-thickness-range =
@ -698,15 +698,15 @@ pdfjs-views-manager-status-undo-cut-label =
# $count (Number) - the number of selected pages to be copied.
pdfjs-views-manager-pages-status-undo-copy-label =
{ $count ->
[one] 1 pagina copiada
*[other] { $count } paginas copiadas
[one] 1 página copiada
*[other] { $count } páginas copiadas
}
# Variables:
# $count (Number) - the number of selected pages to be deleted.
pdfjs-views-manager-pages-status-undo-delete-label =
{ $count ->
[one] 1 pagina eliminada
*[other] { $count } paginas eliminadas
[one] 1 página eliminada
*[other] { $count } páginas eliminadas
}
pdfjs-views-manager-pages-status-waiting-ready-label = Preparando tu archivo…
pdfjs-views-manager-pages-status-waiting-uploading-label = Subiendo archivo…

View File

@ -422,11 +422,11 @@ pdfjs-editor-comments-sidebar-no-comments-link = Argibide gehiago
## Alt-text dialog
pdfjs-editor-alt-text-button-label = Testu alternatiboa
pdfjs-editor-alt-text-button-label = Ordezko testua
pdfjs-editor-alt-text-edit-button =
.aria-label = Editatu testu alternatiboa
.aria-label = Editatu ordezko testua
pdfjs-editor-alt-text-dialog-label = Aukeratu aukera
pdfjs-editor-alt-text-dialog-description = Testu alternatiboak laguntzen du jendeak ezin duenean irudia ikusi edo ez denean kargatzen.
pdfjs-editor-alt-text-dialog-description = Ordezko testuak laguntzen du jendeak ezin duenean irudia ikusi edo ez denean kargatzen.
pdfjs-editor-alt-text-add-description-label = Gehitu azalpena
pdfjs-editor-alt-text-add-description-description = Saiatu idazten gaia, ezarpena edo ekintzak deskribatzen dituen esaldi 1 edo 2.
pdfjs-editor-alt-text-mark-decorative-label = Markatu apaingarri gisa
@ -439,7 +439,7 @@ pdfjs-editor-alt-text-textarea =
.placeholder = Adibidez, "gizon gaztea mahaian eserita dago bazkaltzeko"
# Alternative text (alt text) helps when people can't see the image.
pdfjs-editor-alt-text-button =
.aria-label = Testu alternatiboa
.aria-label = Ordezko testua
## Editor resizers
## This is used in an aria label to help to understand the role of the resizer.
@ -491,38 +491,38 @@ pdfjs-editor-highlight-show-all-button =
## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy.
# Modal header positioned above a text box where users can edit the alt text.
pdfjs-editor-new-alt-text-dialog-edit-label = Editatu testu alternatiboa (irudiaren azalpena)
pdfjs-editor-new-alt-text-dialog-edit-label = Editatu ordezko testua (irudiaren azalpena)
# Modal header positioned above a text box where users can add the alt text.
pdfjs-editor-new-alt-text-dialog-add-label = Gehitu testu alternatiboa (irudiaren azalpena)
pdfjs-editor-new-alt-text-dialog-add-label = Gehitu ordezko testua (irudiaren azalpena)
pdfjs-editor-new-alt-text-textarea =
.placeholder = Idatzi zure azalpena hemen…
# This text refers to the alt text box above this description. It offers a definition of alt text.
pdfjs-editor-new-alt-text-description = Azalpen laburra irudia ikusi ezin duen jendearentzat edo irudia kargatu ezin denerako.
# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human.
pdfjs-editor-new-alt-text-disclaimer1 = Testu alternatibo hau automatikoki sortu da eta okerra izan liteke.
pdfjs-editor-new-alt-text-disclaimer1 = Ordezko testu hau automatikoki sortu da eta okerra izan liteke.
pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Argibide gehiago
pdfjs-editor-new-alt-text-create-automatically-button-label = Sortu testu alternatiboa automatikoki
pdfjs-editor-new-alt-text-create-automatically-button-label = Sortu ordezko testua automatikoki
pdfjs-editor-new-alt-text-not-now-button = Une honetan ez
pdfjs-editor-new-alt-text-error-title = Ezin da testu alternatiboa automatikoki sortu
pdfjs-editor-new-alt-text-error-description = Idatzi zure testu alternatibo propioa edo saiatu berriro geroago.
pdfjs-editor-new-alt-text-error-title = Ezin da ordezko testua automatikoki sortu
pdfjs-editor-new-alt-text-error-description = Idatzi zure ordezko testu propioa edo saiatu berriro geroago.
pdfjs-editor-new-alt-text-error-close-button = Itxi
# Variables:
# $totalSize (Number) - the total size (in MB) of the AI model.
# $downloadedSize (Number) - the downloaded size (in MB) of the AI model.
pdfjs-editor-new-alt-text-ai-model-downloading-progress = Testu alternatiboaren AA modeloa deskargatzen ({ $downloadedSize }/{ $totalSize } MB)
.aria-valuetext = Testu alternatiboaren AA modeloa deskargatzen ({ $downloadedSize }/{ $totalSize } MB)
pdfjs-editor-new-alt-text-ai-model-downloading-progress = Ordezko testuaren AA modeloa deskargatzen ({ $downloadedSize }/{ $totalSize } MB)
.aria-valuetext = Ordezko testuaren AA modeloa deskargatzen ({ $downloadedSize }/{ $totalSize } MB)
# This is a button that users can click to edit the alt text they have already added.
pdfjs-editor-new-alt-text-added-button =
.aria-label = Testu alternatiboa gehituta
pdfjs-editor-new-alt-text-added-button-label = Testu alternatiboa gehituta
.aria-label = Ordezko testua gehituta
pdfjs-editor-new-alt-text-added-button-label = Ordezko testua gehituta
# This is a button that users can click to open the alt text editor and add alt text when it is not present.
pdfjs-editor-new-alt-text-missing-button =
.aria-label = Testu alternatiboa falta da
pdfjs-editor-new-alt-text-missing-button-label = Testu alternatiboa falta da
.aria-label = Ordezko testua falta da
pdfjs-editor-new-alt-text-missing-button-label = Ordezko testua falta da
# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated.
pdfjs-editor-new-alt-text-to-review-button =
.aria-label = Berrikusi testu alternatiboa
pdfjs-editor-new-alt-text-to-review-button-label = Berrikusi testu alternatiboa
.aria-label = Berrikusi ordezko testua
pdfjs-editor-new-alt-text-to-review-button-label = Berrikusi ordezko testua
# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear.
# Variables:
# $generatedAltText (String) - the generated alt-text.
@ -531,22 +531,22 @@ pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Automatikoki sort
## Image alt-text settings
pdfjs-image-alt-text-settings-button =
.title = Irudiaren testu alternatiboaren ezarpenak
pdfjs-image-alt-text-settings-button-label = Irudiaren testu alternatiboaren ezarpenak
pdfjs-editor-alt-text-settings-dialog-label = Irudiaren testu alternatiboaren ezarpenak
pdfjs-editor-alt-text-settings-automatic-title = Testu alternatibo automatikoa
pdfjs-editor-alt-text-settings-create-model-button-label = Sortu testu alternatiboa automatikoki
.title = Irudien ordezko testuaren ezarpenak
pdfjs-image-alt-text-settings-button-label = Irudien ordezko testuaren ezarpenak
pdfjs-editor-alt-text-settings-dialog-label = Irudien ordezko testuaren ezarpenak
pdfjs-editor-alt-text-settings-automatic-title = Ordezko testu automatikoa
pdfjs-editor-alt-text-settings-create-model-button-label = Sortu ordezko testua automatikoki
pdfjs-editor-alt-text-settings-create-model-description = Azalpenak iradokitzen ditu irudia ikusi ezin duen jendearentzat edo irudia kargatu ezin denerako.
# Variables:
# $totalSize (Number) - the total size (in MB) of the AI model.
pdfjs-editor-alt-text-settings-download-model-label = Testu alternatiboaren AA modeloa ({ $totalSize } MB)
pdfjs-editor-alt-text-settings-ai-model-description = Zure gailuan modu lokalean exekutatzen da eta zure datuak pribatu mantentzen dira. Testu alternatibo automatikorako beharrezkoa.
pdfjs-editor-alt-text-settings-download-model-label = Ordezko testuaren AA modeloa ({ $totalSize } MB)
pdfjs-editor-alt-text-settings-ai-model-description = Zure gailuan modu lokalean exekutatzen da eta zure datuak pribatu mantentzen dira. Ordezko testu automatikorako beharrezkoa.
pdfjs-editor-alt-text-settings-delete-model-button = Ezabatu
pdfjs-editor-alt-text-settings-download-model-button = Deskargatu
pdfjs-editor-alt-text-settings-downloading-model-button = Deskargatzen…
pdfjs-editor-alt-text-settings-editor-title = Testu alternatiboaren editorea
pdfjs-editor-alt-text-settings-show-dialog-button-label = Erakutsi testu alternatiboa irudi bat gehitzean berehala
pdfjs-editor-alt-text-settings-show-dialog-description = Zure irudiek testu alternatiboa duela ziurtatzen laguntzen dizu.
pdfjs-editor-alt-text-settings-editor-title = Ordezko testuaren editorea
pdfjs-editor-alt-text-settings-show-dialog-button-label = Erakutsi ordezko testua irudi bat gehitzean berehala
pdfjs-editor-alt-text-settings-show-dialog-description = Zure irudi guztiek ordezko testua dutela ziurtatzen laguntzen dizu.
pdfjs-editor-alt-text-settings-close-button = Itxi
## Accessibility labels (announced by screen readers) for objects added to the editor.
@ -584,7 +584,7 @@ pdfjs-editor-undo-bar-close-button-label = Itxi
pdfjs-editor-add-signature-dialog-label =
Leiho modal honek PDF dokumentu batera gehitzeko sinadurak
sortzea ahalbidetzen dio erabiltzaileari. Erabiltzaileak izena edita
dezake (testu alternatibo modura ere erabiltzen dena) eta sinadura
dezake (ordezko testu modura ere erabiltzen dena) eta sinadura
gordetzeko aukera du gehiagotan erabili ahal izateko.
pdfjs-editor-add-signature-dialog-title = Gehitu sinadura
@ -619,9 +619,9 @@ pdfjs-editor-add-signature-image-browse-link =
## Controls
pdfjs-editor-add-signature-description-label = Azalpena (testu alternatiboa)
pdfjs-editor-add-signature-description-label = Azalpena (ordezko testua)
pdfjs-editor-add-signature-description-input =
.title = Azalpena (testu alternatiboa)
.title = Azalpena (ordezko testua)
pdfjs-editor-add-signature-description-default-when-drawing = Sinadura
pdfjs-editor-add-signature-clear-button-label = Garbitu sinadura
pdfjs-editor-add-signature-clear-button =

View File

@ -257,7 +257,7 @@ pdfjs-find-match-count = Occurrence { $current } sur { $total }
# $limit (Number) - the maximum number of matches
pdfjs-find-match-count-limit =
{ $limit ->
[one] Plus d{ $limit } occurrence
[1] Plus dune occurrence
*[other] Plus de { $limit } occurrences
}
pdfjs-find-not-found = Expression non trouvée

View File

@ -261,7 +261,7 @@ pdfjs-find-match-count =
# $limit (Number) - the maximum number of matches
pdfjs-find-match-count-limit =
{ $limit ->
[one] Più di una { $limit } corrispondenza
[one] Più di { $limit } corrispondenza
*[other] Più di { $limit } corrispondenze
}
pdfjs-find-not-found = Testo non trovato

View File

@ -132,8 +132,8 @@ pdfjs-document-properties-page-size-orientation-portrait = pokončno
pdfjs-document-properties-page-size-orientation-landscape = ležeče
pdfjs-document-properties-page-size-name-a-three = A3
pdfjs-document-properties-page-size-name-a-four = A4
pdfjs-document-properties-page-size-name-letter = Pismo
pdfjs-document-properties-page-size-name-legal = Pravno
pdfjs-document-properties-page-size-name-letter = Letter
pdfjs-document-properties-page-size-name-legal = Legal
## Variables:
## $width (Number) - the width of the (current) page

View File

@ -486,7 +486,7 @@ pdfjs-editor-new-alt-text-description = Mô tả ngắn gọn dành cho người
pdfjs-editor-new-alt-text-disclaimer1 = Văn bản thay thế này được tạo tự động và có thể không chính xác.
pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Tìm hiểu thêm
pdfjs-editor-new-alt-text-create-automatically-button-label = Tạo văn bản thay thế tự động
pdfjs-editor-new-alt-text-not-now-button = Không phải bây giờ
pdfjs-editor-new-alt-text-not-now-button = Để sau
pdfjs-editor-new-alt-text-error-title = Không thể tạo tự động văn bản thay thế
pdfjs-editor-new-alt-text-error-description = Vui lòng viết văn bản thay thế của riêng bạn hoặc thử lại sau.
pdfjs-editor-new-alt-text-error-close-button = Đóng

238
package-lock.json generated
View File

@ -15,8 +15,8 @@
"@fluent/dom": "^0.10.2",
"@metalsmith/layouts": "^3.0.0",
"@metalsmith/markdown": "^1.10.0",
"@napi-rs/canvas": "^1.0.2",
"@types/node": "^26.1.1",
"@napi-rs/canvas": "^1.0.3",
"@types/node": "^26.1.2",
"autoprefixer": "^10.5.4",
"babel-loader": "^10.1.1",
"babel-plugin-istanbul": "^8.0.2",
@ -33,7 +33,7 @@
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-regexp": "^3.1.1",
"eslint-plugin-unicorn": "^72.0.0",
"globals": "^17.7.0",
"globals": "^17.8.0",
"gulp": "^5.0.1",
"gulp-cli": "^3.1.0",
"gulp-postcss": "^10.0.0",
@ -49,14 +49,14 @@
"jstransformer-nunjucks": "^1.2.0",
"kleur": "^4.1.5",
"metalsmith": "^2.7.0",
"metalsmith-html-relative": "^2.0.12",
"metalsmith-html-relative": "^2.0.13",
"ordered-read-streams": "^2.0.0",
"pngjs": "^7.0.0",
"postcss": "^8.5.23",
"postcss": "^8.5.25",
"postcss-discard-comments": "^8.0.1",
"postcss-values-parser": "^8.0.0",
"prettier": "^3.9.6",
"puppeteer": "^25.3.0",
"puppeteer": "^25.4.0",
"stylelint": "^17.14.1",
"stylelint-prettier": "^5.0.3",
"svglint": "^4.2.1",
@ -65,7 +65,7 @@
"ttest": "^4.0.0",
"typescript": "^6.0.3",
"vinyl": "^3.0.1",
"webpack": "^5.109.0",
"webpack": "^5.109.2",
"webpack-stream": "^7.0.0"
},
"engines": {
@ -3669,9 +3669,9 @@
}
},
"node_modules/@napi-rs/canvas": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-1.0.2.tgz",
"integrity": "sha512-EYEqlMYaCbpZDz+IgDH5xp9MTd3ui4dmGqbQYryhMLnSRxrhHKq5KQWHHKxFUcEP4Hp8/BWgvqXocX4j7iSbOQ==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-1.0.3.tgz",
"integrity": "sha512-OlI657a5XXvKGFX7kNeIzJ8rO7IXt87Mqu2H8rXE46viAuOfum/JA7ysX7+eBhxNKznT+RCZh418mndlcFX3+w==",
"dev": true,
"license": "MIT",
"workspaces": [
@ -3685,23 +3685,23 @@
"url": "https://github.com/sponsors/Brooooooklyn"
},
"optionalDependencies": {
"@napi-rs/canvas-android-arm64": "1.0.2",
"@napi-rs/canvas-darwin-arm64": "1.0.2",
"@napi-rs/canvas-darwin-x64": "1.0.2",
"@napi-rs/canvas-linux-arm-gnueabihf": "1.0.2",
"@napi-rs/canvas-linux-arm64-gnu": "1.0.2",
"@napi-rs/canvas-linux-arm64-musl": "1.0.2",
"@napi-rs/canvas-linux-riscv64-gnu": "1.0.2",
"@napi-rs/canvas-linux-x64-gnu": "1.0.2",
"@napi-rs/canvas-linux-x64-musl": "1.0.2",
"@napi-rs/canvas-win32-arm64-msvc": "1.0.2",
"@napi-rs/canvas-win32-x64-msvc": "1.0.2"
"@napi-rs/canvas-android-arm64": "1.0.3",
"@napi-rs/canvas-darwin-arm64": "1.0.3",
"@napi-rs/canvas-darwin-x64": "1.0.3",
"@napi-rs/canvas-linux-arm-gnueabihf": "1.0.3",
"@napi-rs/canvas-linux-arm64-gnu": "1.0.3",
"@napi-rs/canvas-linux-arm64-musl": "1.0.3",
"@napi-rs/canvas-linux-riscv64-gnu": "1.0.3",
"@napi-rs/canvas-linux-x64-gnu": "1.0.3",
"@napi-rs/canvas-linux-x64-musl": "1.0.3",
"@napi-rs/canvas-win32-arm64-msvc": "1.0.3",
"@napi-rs/canvas-win32-x64-msvc": "1.0.3"
}
},
"node_modules/@napi-rs/canvas-android-arm64": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-1.0.2.tgz",
"integrity": "sha512-IMXKVQod0ol4vt3gmClUfXz4JAgHYESGPCUqmH3lQxBoL0K/2greJaQE1HVBVxWWFKfLc4OLZVdxg7kXVyXv+g==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-1.0.3.tgz",
"integrity": "sha512-7kSCdUhoXiO+AaIMXdBGdtp6EctZNkmF62Rea/BmVQlwKaM3bBhOzyGUzxyxz9dv5vdBfpyAaxhSRSJF4kqK4A==",
"cpu": [
"arm64"
],
@ -3720,9 +3720,9 @@
}
},
"node_modules/@napi-rs/canvas-darwin-arm64": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-1.0.2.tgz",
"integrity": "sha512-Sc8tPi6cF+5lqOzCCKFALJHhDiRwyMzTPYm3bbhdXsOunU0lQO5f05ucyOzN2r55I23Hg5bsjH63uSCvWp3EgQ==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-1.0.3.tgz",
"integrity": "sha512-ds14V1BPagLszQyaDTeggny5fNeTCqsUQ5QhFj9VDxSEfzrVxXtdbR0LoFyKa0Siaaw8KvqSk4t7k/WoZJwvbg==",
"cpu": [
"arm64"
],
@ -3741,9 +3741,9 @@
}
},
"node_modules/@napi-rs/canvas-darwin-x64": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-1.0.2.tgz",
"integrity": "sha512-niDXZ9LhKB1zLrUdYB64RHQFDGz9rr0eGx061qtJJU3U20EMMIx28ADF5fVYbhtOgkWQrBjFicfaye1yM0U62A==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-1.0.3.tgz",
"integrity": "sha512-qof3LRAAycmkV2I1izZo9RoSHF8kCQr5O05sFwv0jK8rSdYV6KHVwimo6Qb7RxZj40WHKbLHm5JDaUF0o5XUAA==",
"cpu": [
"x64"
],
@ -3762,9 +3762,9 @@
}
},
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-1.0.2.tgz",
"integrity": "sha512-sgatQL9JxGRH/Amzcvu0P3t8Am3duou74CisfuJ41Dwt8cWy723z/9KZ8LlgmxfypEwEZxSTNFJtU8d281lmhQ==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-1.0.3.tgz",
"integrity": "sha512-FU2kKZLmolHA9+KcUA+l1+xH3WTLUUTQDU/kLv9SEUr2TrRPu94aytOeizFJDHPs/QBcw4QL1mCQhetQXYBbag==",
"cpu": [
"arm"
],
@ -3783,9 +3783,9 @@
}
},
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-1.0.2.tgz",
"integrity": "sha512-dgKuX0peF3xwY6ZF5QxGS4wbfDqpoFAJYXiLSp+guZKARQUKMkRqZSDrXKj7nfrec3UCMzC0PFCPte0ES98AiA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-1.0.3.tgz",
"integrity": "sha512-GVSjntxKeA+/y/ZKf1F+cmUw1WeIkE5aMRPqnZUlBTBvBcrvgWccJAWuYCKPX4QJQwZILIIwhgdAbl51yj6fpA==",
"cpu": [
"arm64"
],
@ -3807,9 +3807,9 @@
}
},
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-1.0.2.tgz",
"integrity": "sha512-qwROoDIC9upfvDoRLuPn2aNg9CGW1x0Ygr4k2Or+8paA9d0qBLwk87U+g8KQpoOviKoPoiwl97kvBYuYD7qZoA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-1.0.3.tgz",
"integrity": "sha512-J51oK/axyZ13kxycumSMfLiDZMdWdOVvqDFI28BpuViZHE3A0bQfr8B5vg8YnPEnqLD3BSn1hkdlh2buspEcNQ==",
"cpu": [
"arm64"
],
@ -3831,9 +3831,9 @@
}
},
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-1.0.2.tgz",
"integrity": "sha512-fXRjnPihdnbO6qy1QQOgxAonb68A0TCEG7rj1x7v7rxNElsE8EVIKIEUTvyDtU+sthYSbX+8e7g3oZiLGnOmxw==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-1.0.3.tgz",
"integrity": "sha512-CtQgQjoVTX67jS9XuCTtJ40Sl7wRLMguoFnnGnfDmCWf7kzKFZVwj5ynqUOIGKFMSB61ZCuQlwPvVNxYTTseaw==",
"cpu": [
"riscv64"
],
@ -3855,9 +3855,9 @@
}
},
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-1.0.2.tgz",
"integrity": "sha512-nPR97DXhbWIAy7yazF3jc06kEPMqYMLmPzFOVNlwKPfIoSChnI+x7dc0hTLaihz3jxrjL6j4BbA7earxfx4X3g==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-1.0.3.tgz",
"integrity": "sha512-jtfzAHFp+FRaR7zGT4jyCe6wUgAG/dVb5A4Apd8FY9jKarntDfUAlJXscugiH7ZF5kKnu7/lHFk9LaDPcrGEVQ==",
"cpu": [
"x64"
],
@ -3879,9 +3879,9 @@
}
},
"node_modules/@napi-rs/canvas-linux-x64-musl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-1.0.2.tgz",
"integrity": "sha512-l7zZY5+jL5qnBZtDz7CoBtY6p7EkHu422g/0zWwrOrzIwWyWxZFRfZZORY1UG7YApymPLx+UbOkN206xXn/c1Q==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-1.0.3.tgz",
"integrity": "sha512-xTzaUCKUHTY4bCGadeeRZggbRVbGUT1petg7Z8r9AJR2+D9Bqu6nQAgqBGC6D47tA70LjaaaLTrJ7wNY1T74dg==",
"cpu": [
"x64"
],
@ -3903,9 +3903,9 @@
}
},
"node_modules/@napi-rs/canvas-win32-arm64-msvc": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-1.0.2.tgz",
"integrity": "sha512-yE0koHCFF4PIbMc2o2SEALhnipz7WBISh5glLvQiomtIoCcW0np3H4Lw93ceJAfJttTTeIIWFbwH84F7EVzjMQ==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-1.0.3.tgz",
"integrity": "sha512-ktVLuBkI6QVOm5BwO/WbdGwxgeetAMJa7TTmR8qBarXF0OU2NKjvjUtPJAl2y8t+zBRczJl/1VOl9gua6WcK2g==",
"cpu": [
"arm64"
],
@ -3924,9 +3924,9 @@
}
},
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-1.0.2.tgz",
"integrity": "sha512-okU8/t2foV6C31n0GtvEMbfD5rOFc70+/6xUNME9Guld29sgSOIGUEDScAWFlcP3k5TYQRl9TNkwJEEjh15w8A==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-1.0.3.tgz",
"integrity": "sha512-SGhlQ8bDjL1Cz2KnsKMasr/5sTcwG/SZkB6WCJxLsmSm/3aS2C+3p39bA7iZ2/94+NkVDySZfbiGoaSZSFHYxA==",
"cpu": [
"x64"
],
@ -4160,9 +4160,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "26.1.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz",
"integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==",
"version": "26.1.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz",
"integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -5647,9 +5647,9 @@
}
},
"node_modules/chromium-bidi": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-16.0.1.tgz",
"integrity": "sha512-J63PGu/9PpeCwLIcKYyzWP6yaVL5pxuBc0shlYCYM8BaAkmlwiQboXO1iNbOgSDbVklEyYFfNEcHD8oOAWacUA==",
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz",
"integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@ -6132,9 +6132,9 @@
}
},
"node_modules/devtools-protocol": {
"version": "0.0.1638949",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1638949.tgz",
"integrity": "sha512-mXwg4Fqnv0WR4iuAT/gYUmctNkjILwXFHyZ+m7Ty1dfr0ezZt2U3gnrrJTfRobJTHoXf+IbuFvFITzLrLFjwJA==",
"version": "0.0.1653615",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1653615.tgz",
"integrity": "sha512-pGVkY3T/qXxAp2nFPodwYqOevk6ncNMSmvL8QfRCx5ZWGd6Vor7AFNmyaA8Zs6uJyP1QAfjuLandCgvSix1BNA==",
"dev": true,
"license": "BSD-3-Clause"
},
@ -6320,9 +6320,9 @@
}
},
"node_modules/enhanced-resolve": {
"version": "5.24.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.3.tgz",
"integrity": "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==",
"version": "5.24.5",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz",
"integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -7448,9 +7448,9 @@
}
},
"node_modules/globals": {
"version": "17.7.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz",
"integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==",
"version": "17.8.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-17.8.0.tgz",
"integrity": "sha512-Zz/LMDZScFmkakeL2cTHzf+PbWKdpU3uclqkZT7TjDG58j5WPt0PpA+n9uPI24fZtlw07q0OtEi84K+umsRzqQ==",
"dev": true,
"license": "MIT",
"engines": {
@ -9279,15 +9279,15 @@
}
},
"node_modules/metalsmith-html-relative": {
"version": "2.0.12",
"resolved": "https://registry.npmjs.org/metalsmith-html-relative/-/metalsmith-html-relative-2.0.12.tgz",
"integrity": "sha512-VYZ0OsUbhTk/ktaDilMbgjAZowHaYXBbAd9O3zz66+iNtcWEDgrrrw2TLjxxHQPB4OCa7KqfRRFgInykTI9aUQ==",
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/metalsmith-html-relative/-/metalsmith-html-relative-2.0.13.tgz",
"integrity": "sha512-jG6aVx5v1cL9/mY72qBknPPF+RZluOSim3zETDxJkQt6+Ejb/AyABmNUResNFwyfh4ZjBcAKxSXgid3PKANsfA==",
"dev": true,
"license": "GPL-3.0-or-later",
"dependencies": {
"cheerio": "^1.2.0",
"deepmerge": "^4.3.1",
"minimatch": "^10.2.5"
"minimatch": "^10.2.6"
},
"engines": {
"node": ">=20.18.1"
@ -9364,13 +9364,13 @@
}
},
"node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"version": "10.2.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
"integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.5"
"brace-expansion": "^5.0.8"
},
"engines": {
"node": "18 || 20 || >=22"
@ -9471,9 +9471,9 @@
}
},
"node_modules/modern-tar": {
"version": "0.7.6",
"resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.6.tgz",
"integrity": "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==",
"version": "0.7.7",
"resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.7.tgz",
"integrity": "sha512-t9VmxaqrmANnEOBhpSDI6HD192Ge48k8vmWqQQL7hSFEqHEYwZbbsu49+aKLWZeRvFs3j1pMhXOqqF4kPlvjkQ==",
"dev": true,
"license": "MIT",
"engines": {
@ -10104,9 +10104,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.23",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz",
"integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==",
"version": "8.5.25",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz",
"integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==",
"dev": true,
"funding": [
{
@ -10339,18 +10339,18 @@
}
},
"node_modules/puppeteer": {
"version": "25.3.0",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.3.0.tgz",
"integrity": "sha512-O1tx8S315aw8eI99HZ5ZNcVEzJ9+jKF//eO5UvfZ3cXJ6okZ5sX3Y50u7DJaM+ewEK4LqXP068tBhfRaWikj+g==",
"version": "25.4.0",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.4.0.tgz",
"integrity": "sha512-xfQp8dFBcGaLc1hEMaVr7s+oW4ZkAurr8Y9H81ilKhu6QoLfSTkZjU7IavnyJ/VWpB9ni3KNJUQHUatslLWyGw==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "3.0.6",
"chromium-bidi": "16.0.1",
"devtools-protocol": "0.0.1638949",
"chromium-bidi": "17.0.2",
"devtools-protocol": "0.0.1653615",
"lilconfig": "^3.1.3",
"puppeteer-core": "25.3.0",
"puppeteer-core": "25.4.0",
"typed-query-selector": "^2.12.2"
},
"bin": {
@ -10361,18 +10361,18 @@
}
},
"node_modules/puppeteer-core": {
"version": "25.3.0",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.3.0.tgz",
"integrity": "sha512-fm+wpUr2oigH1PXZvwgATrM2tYWHMDG8ASzTEe9uukCye4X5Ldx1K5BTHPFKITrIWvQQAQ256d1NpbEveBcKjA==",
"version": "25.4.0",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.4.0.tgz",
"integrity": "sha512-K1plkLOdeoUnGeT1OvdqF3qxl33v+Ra/uH5VyPEhXdMcpvGiEskHzxxEU3fgpccJpJLIipB/rPUsvkZRWeKqOA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "3.0.6",
"chromium-bidi": "16.0.1",
"devtools-protocol": "0.0.1638949",
"chromium-bidi": "17.0.2",
"devtools-protocol": "0.0.1653615",
"typed-query-selector": "^2.12.2",
"webdriver-bidi-protocol": "0.4.2",
"ws": "^8.21.0"
"ws": "^8.21.1"
},
"engines": {
"node": ">=22.12.0"
@ -11974,9 +11974,9 @@
"license": "MIT"
},
"node_modules/test-exclude/node_modules/brace-expansion": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -12761,9 +12761,9 @@
"license": "Apache-2.0"
},
"node_modules/webpack": {
"version": "5.109.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.109.0.tgz",
"integrity": "sha512-vomrngskVVXEZF9sMZfYAd4pXZUnfaWdJGlF+BTNF+gJBCKYCQBnOeVPlrh39Ewl7nlCsirDplMy6o5g9xJHBg==",
"version": "5.109.2",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.109.2.tgz",
"integrity": "sha512-U9/cvLzxObKNEZ9+TtdqrHM5/9z3lgl2c+c4BzbqGxFQvQvBAq87yql5A8pQ+rrMbS496MZJeF5enVBndIy2hw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -12775,7 +12775,7 @@
"acorn": "^8.16.0",
"browserslist": "^4.28.1",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.24.2",
"enhanced-resolve": "^5.24.4",
"es-module-lexer": "^2.1.0",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
@ -13162,9 +13162,9 @@
}
},
"node_modules/ws": {
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"version": "8.21.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
"integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
"dev": true,
"license": "MIT",
"engines": {
@ -13240,16 +13240,16 @@
}
},
"node_modules/yargs": {
"version": "18.0.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
"integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
"version": "18.1.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz",
"integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cliui": "^9.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"string-width": "^7.2.0",
"string-width": "^8.2.1",
"y18n": "^5.0.5",
"yargs-parser": "^22.0.0"
},
@ -13280,26 +13280,18 @@
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/yargs/node_modules/emoji-regex": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"dev": true,
"license": "MIT"
},
"node_modules/yargs/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz",
"integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
"get-east-asian-width": "^1.5.0",
"strip-ansi": "^7.1.2"
},
"engines": {
"node": ">=18"
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"

View File

@ -10,8 +10,8 @@
"@fluent/dom": "^0.10.2",
"@metalsmith/layouts": "^3.0.0",
"@metalsmith/markdown": "^1.10.0",
"@napi-rs/canvas": "^1.0.2",
"@types/node": "^26.1.1",
"@napi-rs/canvas": "^1.0.3",
"@types/node": "^26.1.2",
"autoprefixer": "^10.5.4",
"babel-loader": "^10.1.1",
"babel-plugin-istanbul": "^8.0.2",
@ -28,7 +28,7 @@
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-regexp": "^3.1.1",
"eslint-plugin-unicorn": "^72.0.0",
"globals": "^17.7.0",
"globals": "^17.8.0",
"gulp": "^5.0.1",
"gulp-cli": "^3.1.0",
"gulp-postcss": "^10.0.0",
@ -44,14 +44,14 @@
"jstransformer-nunjucks": "^1.2.0",
"kleur": "^4.1.5",
"metalsmith": "^2.7.0",
"metalsmith-html-relative": "^2.0.12",
"metalsmith-html-relative": "^2.0.13",
"ordered-read-streams": "^2.0.0",
"pngjs": "^7.0.0",
"postcss": "^8.5.23",
"postcss": "^8.5.25",
"postcss-discard-comments": "^8.0.1",
"postcss-values-parser": "^8.0.0",
"prettier": "^3.9.6",
"puppeteer": "^25.3.0",
"puppeteer": "^25.4.0",
"stylelint": "^17.14.1",
"stylelint-prettier": "^5.0.3",
"svglint": "^4.2.1",
@ -60,7 +60,7 @@
"ttest": "^4.0.0",
"typescript": "^6.0.3",
"vinyl": "^3.0.1",
"webpack": "^5.109.0",
"webpack": "^5.109.2",
"webpack-stream": "^7.0.0"
},
"repository": {

View File

@ -1,5 +1,5 @@
{
"stableVersion": "6.1.200",
"baseVersion": "eddd70a2ca1054ad2e0792972c3f2774b89f0cd2",
"versionPrefix": "6.2."
"stableVersion": "6.2.108",
"baseVersion": "ce4ff55faaa83b39b0137dc458af6eea6f96235f",
"versionPrefix": "6.3."
}

View File

@ -2887,10 +2887,7 @@ class TextWidgetAnnotation extends WidgetAnnotation {
this.data.doNotScroll = this.hasFieldFlag(AnnotationFieldFlag.DONOTSCROLL);
// Check if we have a date or time.
const {
data: { actions },
} = this;
const { actions } = this.data;
if (!actions) {
return;
}
@ -2898,32 +2895,35 @@ class TextWidgetAnnotation extends WidgetAnnotation {
const AFDateTime =
/^AF(Date|Time)_(?:Keystroke|Format)(?:Ex)?\(['"]?([^'"]+)['"]?\);$/;
let canUseHTMLDateTime = false;
const aFormat = actions.get("Format"),
aKeystroke = actions.get("Keystroke");
if (
(actions.Format?.length === 1 &&
actions.Keystroke?.length === 1 &&
AFDateTime.test(actions.Format[0]) &&
AFDateTime.test(actions.Keystroke[0])) ||
(actions.Format?.length === 0 &&
actions.Keystroke?.length === 1 &&
AFDateTime.test(actions.Keystroke[0])) ||
(actions.Keystroke?.length === 0 &&
actions.Format?.length === 1 &&
AFDateTime.test(actions.Format[0]))
(aFormat?.length === 1 &&
aKeystroke?.length === 1 &&
AFDateTime.test(aFormat[0]) &&
AFDateTime.test(aKeystroke[0])) ||
(aFormat?.length === 0 &&
aKeystroke?.length === 1 &&
AFDateTime.test(aKeystroke[0])) ||
(aKeystroke?.length === 0 &&
aFormat?.length === 1 &&
AFDateTime.test(aFormat[0]))
) {
// If the Format and Keystroke actions are the same, we can just use
// the Format action.
canUseHTMLDateTime = true;
}
const actionsToVisit = [];
if (actions.Format) {
actionsToVisit.push(...actions.Format);
if (aFormat) {
actionsToVisit.push(...aFormat);
}
if (actions.Keystroke) {
actionsToVisit.push(...actions.Keystroke);
if (aKeystroke) {
actionsToVisit.push(...aKeystroke);
}
if (canUseHTMLDateTime) {
delete actions.Keystroke;
actions.Format = actionsToVisit;
actions.delete("Keystroke");
actions.set("Format", actionsToVisit);
}
for (const formatAction of actionsToVisit) {

View File

@ -19,6 +19,7 @@ import {
DocumentActionEventType,
FormatError,
info,
makeArr,
PermissionFlag,
shadow,
stringToUTF8String,
@ -1287,10 +1288,10 @@ class Catalog {
);
if (javaScript) {
actions ??= Object.create(null);
actions ??= new Map();
for (const [key, val] of javaScript) {
(actions[key] ??= []).push(val);
actions.getOrInsertComputed(key, makeArr).push(val);
}
}
return shadow(this, "jsActions", actions);

View File

@ -1428,10 +1428,9 @@ class CFFFDSelect {
}
getFDIndex(glyphIndex) {
if (glyphIndex < 0 || glyphIndex >= this.fdSelect.length) {
return -1;
}
return this.fdSelect[glyphIndex];
return glyphIndex < 0 || glyphIndex >= this.fdSelect.length
? -1
: this.fdSelect[glyphIndex];
}
}

View File

@ -239,10 +239,10 @@ class ChunkedStream extends Stream {
};
Object.defineProperty(ChunkedStreamSubstream.prototype, "isDataLoaded", {
get() {
if (this.numChunksLoaded === this.numChunks) {
return true;
}
return this.getMissingChunks().length === 0;
return (
this.numChunksLoaded === this.numChunks ||
this.getMissingChunks().length === 0
);
},
configurable: true,
});

View File

@ -23,6 +23,7 @@ import {
} from "../shared/util.js";
import { Dict, isName, isRefsEqual, Name, Ref, RefSet } from "./primitives.js";
import { BaseStream } from "./base_stream.js";
import { CONTROL_CHAR_REGEXP } from "../shared/css_utils.js";
import { stringToPDFString } from "./string_utils.js";
const PDF_VERSION_REGEXP = /^[1-9]\.\d$/;
@ -341,7 +342,8 @@ function lookupNormalRect(arr, fallback) {
* each part of the path.
*/
function parseXFAPath(path) {
const positionPattern = /(.+)\[(\d+)\]$/;
// Anchoring prevents retrying the match at every character.
const positionPattern = /^(.+)\[(\d+)\]$/;
return path.split(".").map(component => {
const m = component.match(positionPattern);
if (m) {
@ -449,7 +451,7 @@ function _collectJS(entry, xref, list, parents) {
}
function collectActions(xref, dict, eventType) {
const actions = Object.create(null);
const actions = new Map();
const additionalActionsDicts = getInheritableProperty({
dict,
key: "AA",
@ -475,7 +477,7 @@ function collectActions(xref, dict, eventType) {
const list = [];
_collectJS(rawActionDict, xref, list, parents);
if (list.length > 0) {
actions[action] = list;
actions.set(action, list);
}
}
}
@ -487,10 +489,10 @@ function collectActions(xref, dict, eventType) {
const list = [];
_collectJS(actionDict, xref, list, parents);
if (list.length > 0) {
actions.Action = list;
actions.set("Action", list);
}
}
return Object.keys(actions).length ? actions : null;
return actions.size ? actions : null;
}
const XMLEntities = {
@ -560,6 +562,17 @@ function validateFontName(fontFamily, mustWarn = false) {
}
return false;
}
// A <string> is terminated by a newline, which for CSS also includes the
// form feed character; see https://drafts.csswg.org/css-syntax/#newline.
// The font family is escaped before being used, see `serializeFontFamily`,
// hence this only prevents values that cannot sensibly name a font from
// being used at all (the unquoted case below is already this strict).
if (CONTROL_CHAR_REGEXP.test(fontFamily)) {
if (mustWarn) {
warn(`FontFamily contains control characters: ${fontFamily}.`);
}
return false;
}
} else {
// See https://developer.mozilla.org/en-US/docs/Web/CSS/custom-ident.
for (const ident of fontFamily.split(/[ \t]+/)) {
@ -574,6 +587,18 @@ function validateFontName(fontFamily, mustWarn = false) {
return true;
}
// Strip the spaces preceding a digit, since e.g. "Wingdings 3" is not a valid
// font name in the css specs.
// The optional trailing digit is matched as part of the space run, so that a
// failing match cannot backtrack over the spaces; otherwise the replacement
// would be quadratic in the number of consecutive spaces.
function normalizeCSSFontFamily(fontFamily) {
return fontFamily.replaceAll(
/( +)(\d)?/g,
(_, spaces, digit) => digit ?? " "
);
}
function validateCSSFont(cssFontInfo) {
// See https://developer.mozilla.org/en-US/docs/Web/CSS/font-style.
const DEFAULT_CSS_FONT_OBLIQUE = "14";
@ -750,6 +775,7 @@ export {
lookupRect,
MAX_INT_32,
MissingDataException,
normalizeCSSFontFamily,
numberToString,
ParserEOFException,
parseXFAPath,

View File

@ -111,10 +111,9 @@ class DecodeStream extends BaseStream {
async getImageData(length, decoderOptions) {
if (!this.canAsyncDecodeImageFromBuffer) {
if (this.isAsyncDecoder) {
return this.decodeImage(null, length, decoderOptions);
}
return this.getBytes(length, decoderOptions);
return this.isAsyncDecoder
? this.decodeImage(null, length, decoderOptions)
: this.getBytes(length, decoderOptions);
}
const data = await this.stream.asyncGetBytes();
return this.decodeImage(data, length, decoderOptions);

View File

@ -42,6 +42,7 @@ import {
isWhiteSpace,
lookupNormalRect,
MissingDataException,
normalizeCSSFontFamily,
PDF_VERSION_REGEXP,
RESOURCES_KEYS_OPERATOR_LIST,
RESOURCES_KEYS_TEXT_CONTENT,
@ -1396,9 +1397,7 @@ class PDFDocument {
if (!(descriptor instanceof Dict)) {
continue;
}
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");
const fontFamily = normalizeCSSFontFamily(descriptor.get("FontFamily"));
const fontWeight = descriptor.get("FontWeight");
// Angle is expressed in degrees counterclockwise in PDF
@ -1962,7 +1961,7 @@ class PDFDocument {
const { acroForm } = annotationGlobals;
const visitedRefs = new RefSet();
const allFields = Object.create(null);
const allFields = new Map();
const fieldPromises = new Map();
const orphanFields = new RefSetCache();
for (const fieldRef of acroForm.get("Fields")) {
@ -1983,7 +1982,7 @@ class PDFDocument {
Promise.all(promises).then(fields => {
fields = fields.filter(field => !!field);
if (fields.length > 0) {
allFields[name] = fields;
allFields.set(name, fields);
}
})
);
@ -1991,7 +1990,7 @@ class PDFDocument {
await Promise.all(allPromises);
return {
allFields: Object.keys(allFields).length ? allFields : null,
allFields: allFields.size ? allFields : null,
orphanFields,
};
});
@ -2269,9 +2268,9 @@ class PDFDocument {
return true;
}
if (fieldObjects?.allFields) {
return Object.values(fieldObjects.allFields).some(fieldObject =>
fieldObject.some(object => object.actions !== null)
);
return fieldObjects.allFields
.values()
.some(fieldObj => fieldObj.some(obj => obj.actions !== null));
}
return false;
}

View File

@ -263,7 +263,7 @@ class PDFEditor {
) {
if (obj instanceof Ref) {
const {
currentDocument: { oldRefMapping },
currentDocument: { fieldToParent, oldRefMapping },
} = this;
const existingRef = oldRefMapping.get(obj);
if (existingRef) {
@ -271,6 +271,12 @@ class PDFEditor {
}
const oldRef = obj;
obj = await xref.fetchAsync(oldRef);
const mappedRef = oldRefMapping.get(oldRef);
if (mappedRef) {
// Another concurrent traversal may have allocated the clone while the
// source object was being fetched.
return mappedRef;
}
if (typeof obj === "number") {
// Simple value; no need to create a new reference.
return obj;
@ -299,9 +305,17 @@ class PDFEditor {
}
}
let cloneSource = true;
if (fieldToParent.has(oldRef) && obj instanceof Dict) {
// Avoid following a widget's field hierarchy while cloning the page,
// without mutating the source dictionary cached by its XRef.
obj = this.cloneDict(obj);
obj.delete("Parent");
cloneSource = false;
}
this.xref[newRef.num] = await this.#collectDependencies(
obj,
true,
cloneSource,
xref,
resourceStreamPath
);
@ -1223,11 +1237,8 @@ class PDFEditor {
"Sig"
);
const parentRef = annotationDict.getRaw("Parent") || null;
// We remove the parent to avoid visiting it when cloning the
// annotation.
// It'll be fixed later in #mergeAcroForms when merging the
// AcroForms.
annotationDict.delete("Parent");
// The parent will be omitted from the annotation clone to avoid
// visiting it, then restored by #mergeAcroForms.
fieldToParent.put(annotationRef, parentRef);
}

View File

@ -186,10 +186,7 @@ class Parser {
}
if (typeof buf1 === "string") {
if (cipherTransform) {
return cipherTransform.decryptString(buf1);
}
return buf1;
return cipherTransform ? cipherTransform.decryptString(buf1) : buf1;
}
// simple object

View File

@ -39,10 +39,7 @@ class Stream extends BaseStream {
}
getByte() {
if (this.pos >= this.end) {
return -1;
}
return this.bytes[this.pos++];
return this.pos >= this.end ? -1 : this.bytes[this.pos++];
}
getBytes(length) {

View File

@ -16,19 +16,15 @@
import { stringToBytes, Util, warn } from "../shared/util.js";
function isAscii(str) {
return (
typeof str === "string" &&
// eslint-disable-next-line no-control-regex
(!str || /^[\x00-\x7F]*$/.test(str))
);
return typeof str === "string" && (!str || /^[\x00-\x7F]*$/.test(str));
}
// If the string is null or undefined then it is returned as is.
function stringToAsciiOrUTF16BE(str) {
if (str === null || str === undefined) {
return str;
}
return isAscii(str) ? str : stringToUTF16String(str, /* bigEndian = */ true);
return str === null || str === undefined || isAscii(str)
? str
: stringToUTF16String(str, /* bigEndian = */ true);
}
function stringToUTF16HexString(str) {

View File

@ -83,10 +83,9 @@ class IdentityToUnicodeMap {
}
get(i) {
if (this.firstChar <= i && i <= this.lastChar) {
return String.fromCharCode(i);
}
return undefined;
return this.firstChar <= i && i <= this.lastChar
? String.fromCharCode(i)
: undefined;
}
charCodeOf(v) {

View File

@ -148,6 +148,31 @@ async function writeArray(array, buffer, transform) {
buffer.push("]");
}
// The exponential notation isn't valid in a PDF, hence a number is always
// written with all its digits.
function numberToPDFString(value) {
// `toFixed` uses the exponential notation from 1e21 on, so such a number is
// written thanks to BigInt: it's necessarily an integer, and `isInteger` also
// rules out NaN and ±Infinity for which BigInt would throw.
if (Number.isInteger(value) && Math.abs(value) >= 1e21) {
return BigInt(value).toString();
}
// Below that limit `toFixed(10)` never uses the exponential notation (unlike
// `toString` which uses it under 1e-6) and it rounds the value: it always
// adds 10 decimals, hence scan them backwards to remove the trailing zeros,
// and then the dot itself when none of the decimals is left.
const str = value.toFixed(10);
let end = str.length;
while (str[end - 1] === "0") {
end--;
}
if (str[end - 1] === ".") {
end--;
}
return str.slice(0, end);
}
async function writeValue(value, buffer, transform) {
if (value instanceof Name) {
buffer.push(`/${escapePDFName(value.name)}`);
@ -165,9 +190,7 @@ async function writeValue(value, buffer, transform) {
// matrices (e.g. [0.000008 0 0 0.000008 0 0]).
// The numbers must be "rounded" only when pdf.js is producing them and the
// current transformation matrix is well known.
// toFixed(10) avoids scientific notation and rounds; the replace removes
// trailing zeros (and a trailing dot for integers).
buffer.push(value.toFixed(10).replace(/\.?0+$/, ""));
buffer.push(numberToPDFString(value));
} else if (typeof value === "boolean") {
buffer.push(value.toString());
} else if (value instanceof Dict) {

View File

@ -190,11 +190,9 @@ class Builder {
if (hasNamespace) {
this._currentNamespace = this._namespaceStack.pop();
}
if (prefixes) {
prefixes.forEach(({ prefix }) => {
prefixes?.forEach(({ prefix }) => {
this._namespacePrefixes.get(prefix).pop();
});
}
if (nsAgnostic) {
this._nsAgnosticLevel--;
}

View File

@ -154,10 +154,7 @@ class FontFinder {
function selectFont(xfaFont, typeface) {
if (xfaFont.posture === "italic") {
if (xfaFont.weight === "bold") {
return typeface.bolditalic;
}
return typeface.italic;
return xfaFont.weight === "bold" ? typeface.bolditalic : typeface.italic;
} else if (xfaFont.weight === "bold") {
return typeface.bold;
}

View File

@ -28,6 +28,7 @@ import {
import { createValidAbsoluteUrl, warn } from "../../shared/util.js";
import { getMeasurement, stripQuotes } from "./utils.js";
import { selectFont } from "./fonts.js";
import { serializeFontFamily } from "../../shared/css_utils.js";
import { TextMeasure } from "./text.js";
import { XFAObject } from "./xfa_object.js";
@ -597,13 +598,16 @@ function setFontFamily(xfaFont, node, fontFinder, style) {
}
const name = stripQuotes(xfaFont.typeface);
style.fontFamily = `"${name}"`;
// Use the same serialization as the `@font-face` rule, resp. the `FontFace`
// instance, that the font is registered with; see `createFontFaceRule` and
// `createNativeFontFace` in `src/display/font_loader.js`.
style.fontFamily = serializeFontFamily(name);
const typeface = fontFinder.find(name);
if (typeface) {
const { fontFamily } = typeface.regular.cssFontInfo;
if (fontFamily !== name) {
style.fontFamily = `"${fontFamily}"`;
style.fontFamily = serializeFontFamily(fontFamily);
}
const para = getCurrentPara(node);

View File

@ -6073,10 +6073,9 @@ class Value extends XFAObject {
[$text]() {
if (this.exData) {
if (typeof this.exData[$content] === "string") {
return this.exData[$content].trim();
}
return this.exData[$content][$text]().trim();
return typeof this.exData[$content] === "string"
? this.exData[$content].trim()
: this.exData[$content][$text]().trim();
}
for (const name of Object.getOwnPropertyNames(this)) {
if (name === "image") {

View File

@ -284,10 +284,9 @@ class XFAObject {
}
[$text]() {
if (this[_children].length === 0) {
return this[$content];
}
return this[_children].map(c => c[$text]()).join("");
return this[_children].length === 0
? this[$content]
: this[_children].map(c => c[$text]()).join("");
}
get [_attributeNames]() {
@ -329,11 +328,7 @@ class XFAObject {
}
[$getChildren](name = null) {
if (!name) {
return this[_children];
}
return this[name];
return !name ? this[_children] : this[name];
}
[$dump]() {
@ -680,11 +675,9 @@ class XFAObject {
}
[$getChildren](name = null) {
if (!name) {
return this[_children];
}
return this[_children].filter(c => c[$nodeName] === name);
return !name
? this[_children]
: this[_children].filter(c => c[$nodeName] === name);
}
[$getChildrenByClass](name) {
@ -909,11 +902,9 @@ class XmlObject extends XFAObject {
}
[$getChildren](name = null) {
if (!name) {
return this[_children];
}
return this[_children].filter(c => c[$nodeName] === name);
return !name
? this[_children]
: this[_children].filter(c => c[$nodeName] === name);
}
[$getAttributes]() {

View File

@ -49,16 +49,17 @@ function isWhitespaceString(s) {
class XMLParserBase {
static get _entityRegex() {
return shadow(this, "_entityRegex", /&(?:#x([^;]+)|#([^;]+)|([^;]+));/g);
// Entity references cannot contain "&", keeping the scan linear.
return shadow(this, "_entityRegex", /&(?:#x([^;&]+)|#([^;&]+)|([^;&]+));/g);
}
_resolveEntities(s) {
return s.replaceAll(XMLParserBase._entityRegex, (_, hex, dec, entity) => {
if (hex) {
return String.fromCodePoint(parseInt(hex, 16));
}
if (dec) {
return String.fromCodePoint(parseInt(dec, 10));
return s.replaceAll(XMLParserBase._entityRegex, (all, hex, dec, entity) => {
if (hex || dec) {
const code = hex ? parseInt(hex, 16) : parseInt(dec, 10);
// An out-of-range or unparsable code point is kept as-is, since
// `String.fromCodePoint` would throw on it.
return code >= 0 && code <= 0x10ffff ? String.fromCodePoint(code) : all;
}
switch (entity) {
case "lt":
@ -324,10 +325,9 @@ class SimpleDOMNode {
}
get textContent() {
if (!this.childNodes) {
return this.nodeValue || "";
}
return this.childNodes.map(child => child.textContent).join("");
return !this.childNodes
? this.nodeValue || ""
: this.childNodes.map(child => child.textContent).join("");
}
get children() {

View File

@ -75,7 +75,7 @@ const TIMEZONE_OFFSET = new Date().getTimezoneOffset() * 60 * 1000;
* @property {Object} svgFactory
* @property {boolean} [enableScripting]
* @property {boolean} [hasJSActions]
* @property {Object} [fieldObjects]
* @property {Map} [fieldObjects]
*/
class AnnotationElementFactory {
@ -803,7 +803,7 @@ class AnnotationElement {
const fields = [];
if (this._fieldObjects) {
const fieldObj = this._fieldObjects[name] || [];
const fieldObj = this._fieldObjects.get(name) || [];
for (const { page, id, exportValues } of fieldObj) {
if (page === -1) {
@ -1012,9 +1012,9 @@ class LinkAnnotationElement extends AnnotationElement {
} else {
if (
data.actions &&
(data.actions.Action ||
data.actions["Mouse Up"] ||
data.actions["Mouse Down"]) &&
(data.actions.has("Action") ||
data.actions.has("Mouse Up") ||
data.actions.has("Mouse Down")) &&
this.enableScripting &&
this.hasJSActions
) {
@ -1158,14 +1158,14 @@ class LinkAnnotationElement extends AnnotationElement {
* @param {Object} data
* @memberof LinkAnnotationElement
*/
_bindJSAction(link, data) {
_bindJSAction(link, { actions, id, overlaidText }) {
link.href = this.linkService.getAnchorUrl("");
const map = new Map([
["Action", "onclick"],
["Mouse Up", "onmouseup"],
["Mouse Down", "onmousedown"],
]);
for (const name of Object.keys(data.actions)) {
for (const name of actions.keys()) {
const jsName = map.get(name);
if (!jsName) {
continue;
@ -1173,16 +1173,13 @@ class LinkAnnotationElement extends AnnotationElement {
link[jsName] = () => {
this.linkService.eventBus?.dispatch("dispatcheventinsandbox", {
source: this,
detail: {
id: data.id,
name,
},
detail: { id, name },
});
return false;
};
}
if (data.overlaidText) {
link.title = data.overlaidText;
if (overlaidText) {
link.title = overlaidText;
}
link.onclick ||= () => false;
@ -1220,12 +1217,12 @@ class LinkAnnotationElement extends AnnotationElement {
if (resetFormFields.length !== 0 || resetFormRefs.length !== 0) {
const fieldIds = new Set(resetFormRefs);
for (const fieldName of resetFormFields) {
const fields = this._fieldObjects[fieldName] || [];
const fields = this._fieldObjects.get(fieldName) || [];
for (const { id } of fields) {
fieldIds.add(id);
}
}
for (const fields of Object.values(this._fieldObjects)) {
for (const fields of this._fieldObjects.values()) {
for (const field of fields) {
if (fieldIds.has(field.id) === include) {
allFields.push(field);
@ -1233,7 +1230,7 @@ class LinkAnnotationElement extends AnnotationElement {
}
}
} else {
for (const fields of Object.values(this._fieldObjects)) {
for (const fields of this._fieldObjects.values()) {
allFields.push(...fields);
}
}
@ -1379,8 +1376,10 @@ class WidgetAnnotationElement extends AnnotationElement {
}
_setEventListeners(element, elementData, names, getter) {
const { actions } = this.data;
for (const [baseName, eventName] of names) {
if (eventName === "Action" || this.data.actions?.[eventName]) {
if (eventName === "Action" || actions?.has(eventName)) {
if (eventName === "Focus" || eventName === "Blur") {
elementData ||= { focused: false };
}
@ -1391,10 +1390,10 @@ class WidgetAnnotationElement extends AnnotationElement {
eventName,
getter
);
if (eventName === "Focus" && !this.data.actions?.Blur) {
if (eventName === "Focus" && !actions?.has("Blur")) {
// Ensure that elementData will have the correct value.
this._setEventListener(element, elementData, "blur", "Blur", null);
} else if (eventName === "Blur" && !this.data.actions?.Focus) {
} else if (eventName === "Blur" && !actions?.has("Focus")) {
this._setEventListener(element, elementData, "focus", "Focus", null);
}
}
@ -1630,7 +1629,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
}
elementData.lastCommittedValue = target.value;
elementData.commitKey = 1;
if (!this.data.actions?.Focus) {
if (!this.data.actions?.has("Focus")) {
elementData.focused = true;
}
});
@ -1752,7 +1751,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
if (!elementData.focused || !event.relatedTarget) {
return;
}
if (!this.data.actions?.Blur) {
if (!this.data.actions?.has("Blur")) {
elementData.focused = false;
}
const { target } = event;
@ -1800,7 +1799,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
_blurListener(event);
});
if (this.data.actions?.Keystroke) {
if (this.data.actions?.has("Keystroke")) {
element.addEventListener("beforeinput", event => {
elementData.lastCommittedValue = null;
const { data, target } = event;
@ -1812,11 +1811,21 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
switch (event.inputType) {
// https://rawgit.com/w3c/input-events/v1/index.html#interface-InputEvent-Attributes
case "deleteWordBackward": {
const match = value
.substring(0, selectionStart)
.match(/\w*\W*$/);
if (match) {
selStart -= match[0].length;
// The previous unanchored regex could take quadratic time, so
// scan backwards over the trailing non-word characters and
// then the word.
const wordCharPattern = /\w/;
while (
selStart > 0 &&
!wordCharPattern.test(value[selStart - 1])
) {
selStart--;
}
while (
selStart > 0 &&
wordCharPattern.test(value[selStart - 1])
) {
selStart--;
}
break;
}
@ -3123,10 +3132,7 @@ class PopupElement {
}
get isVisible() {
if (this.#commentManager) {
return false;
}
return this.#container.hidden === false;
return !this.#commentManager && this.#container.hidden === false;
}
}
@ -3947,7 +3953,7 @@ class MediaAnnotationElement extends AnnotationElement {
* @property {boolean} [enableScripting] - Enable embedded script execution.
* @property {boolean} [hasJSActions] - Some fields have JS actions.
* The default value is `false`.
* @property {Object<string, Array<Object>> | null} [fieldObjects]
* @property {Map<string, Array<Object>> | null} [fieldObjects]
* @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap]
* @property {TextAccessibilityManager} [accessibilityManager]
* @property {AnnotationEditorUIManager} [annotationEditorUIManager]

View File

@ -867,8 +867,8 @@ class PDFDocumentProxy {
}
/**
* @returns {Promise<Object | null>} A promise that is resolved with
* an {Object} with the JavaScript actions:
* @returns {Promise<Map | null>} A promise that is resolved with a {Map} with
* the JavaScript actions:
* - from the name tree.
* - from A or AA entries in the catalog dictionary.
* , or `null` if no JavaScript exists.
@ -1069,9 +1069,9 @@ class PDFDocumentProxy {
}
/**
* @returns {Promise<Object<string, Array<Object>> | null>} A promise that is
* resolved with an {Object} containing /AcroForm field data for the JS
* sandbox, or `null` when no field data is present in the PDF file.
* @returns {Promise<Map<string, Array<Object>> | null>} A promise that is
* resolved with a {Map} containing /AcroForm field data for the JS sandbox,
* or `null` when no field data is present in the PDF file.
*/
getFieldObjects() {
return this._transport.getFieldObjects();
@ -1429,8 +1429,8 @@ class PDFPageProxy {
}
/**
* @returns {Promise<Object>} A promise that is resolved with an
* {Object} with JS actions.
* @returns {Promise<Map | null>} A promise that is resolved with a {Map} with
* the JavaScript actions, or `null` if no JavaScript exists.
*/
getJSActions() {
return this._transport.getPageJSActions(this._pageIndex);

View File

@ -188,10 +188,7 @@ class CanvasBBoxTracker {
}
getOpenMarker() {
if (this._savesStack.length === 0) {
return null;
}
return this._savesStack.at(-1);
return this._savesStack.length === 0 ? null : this._savesStack.at(-1);
}
recordCloseMarker(opIdx, onSavePopped) {

View File

@ -188,10 +188,22 @@ function getPdfFilenameFromUrl(url, defaultFilename = "document.pdf") {
}
if (newURL.hash) {
const reFilename = /[^/?#=]+\.pdf\b(?!.*\.pdf\b)/i;
const hashFilename = reFilename.exec(newURL.hash);
if (hashFilename) {
return decode(hashFilename[0]);
// Locate the last ".pdf" and then extend it to the left, up to the closest
// separator. Both steps are linear, whereas a single pattern starting with
// `[^/?#=]+` is quadratic on a hash which contains no ".pdf" at all.
const { hash } = newURL;
let extensionStart = -1;
for (const { index } of hash.matchAll(/\.pdf\b/gi)) {
extensionStart = index;
}
if (extensionStart > 0) {
let filenameStart = extensionStart;
while (filenameStart > 0 && !"/?#=".includes(hash[filenameStart - 1])) {
filenameStart--;
}
if (filenameStart < extensionStart) {
return decode(hash.slice(filenameStart, extensionStart + 4));
}
}
}

View File

@ -641,7 +641,8 @@ class DrawLayer {
textLayerData.selectionDiv = div;
}
if (!div.parentNode && drawLayer.#parent) {
if (drawLayer.#parent && div.parentNode !== drawLayer.#parent) {
// The div can still be in a canvas wrapper which has been removed.
drawLayer.#parent.append(div);
this.#selections.add(div);
}

View File

@ -131,17 +131,15 @@ class AltText {
}
isEmpty() {
if (this.#useNewAltTextFlow) {
return this.#altText === null;
}
return !this.#altText && !this.#altTextDecorative;
return this.#useNewAltTextFlow
? this.#altText === null
: !this.#altText && !this.#altTextDecorative;
}
hasData() {
if (this.#useNewAltTextFlow) {
return this.#altText !== null || !!this.#guessedText;
}
return this.isEmpty();
return this.#useNewAltTextFlow
? this.#altText !== null || !!this.#guessedText
: this.isEmpty();
}
get guessedText() {

View File

@ -622,10 +622,7 @@ class InkDrawOutline extends Outline {
}
updateProperty(name, value) {
if (name === "stroke-width") {
return this.#updateThickness(value);
}
return null;
return name === "stroke-width" ? this.#updateThickness(value) : null;
}
#updateThickness(thickness) {

View File

@ -1359,16 +1359,7 @@ class AnnotationEditor {
bindEvents(this, div, ["keydown", "pointerdown", "dblclick"]);
if (this.isResizable && this._uiManager._supportsPinchToZoom) {
this.#touchManager ||= new TouchManager({
container: div,
isPinchingDisabled: () => !this.isSelected,
onPinchStart: this.#touchPinchStartCallback.bind(this),
onPinching: this.#touchPinchCallback.bind(this),
onPinchEnd: this.#touchPinchEndCallback.bind(this),
signal: this._uiManager._signal,
});
}
this.#addTouchManager();
this.addStandaloneCommentButton();
this._uiManager._editorUndoBar?.hide();
@ -1803,6 +1794,25 @@ class AnnotationEditor {
this.div.addEventListener("focusout", this.focusout.bind(this), { signal });
}
#addTouchManager() {
if (
this.#touchManager ||
!this.div ||
!this.isResizable ||
!this._uiManager._supportsPinchToZoom
) {
return;
}
this.#touchManager = new TouchManager({
container: this.div,
isPinchingDisabled: () => !this.isSelected,
onPinchStart: this.#touchPinchStartCallback.bind(this),
onPinching: this.#touchPinchCallback.bind(this),
onPinchEnd: this.#touchPinchEndCallback.bind(this),
signal: this._uiManager._signal,
});
}
/**
* Rebuild the editor in case it has been removed on undo.
*
@ -1810,6 +1820,7 @@ class AnnotationEditor {
*/
rebuild() {
this.#addFocusListeners();
this.#addTouchManager();
}
/**

View File

@ -254,10 +254,9 @@ class SignatureEditor extends DrawingEditor {
/** @inheritdoc */
get toolbarButtons() {
if (this._uiManager.signatureManager) {
return [["editSignature", this._uiManager.signatureManager]];
}
return super.toolbarButtons;
return this._uiManager.signatureManager
? [["editSignature", this._uiManager.signatureManager]]
: super.toolbarButtons;
}
addSignature(data, heightInPage, description, uuid) {

View File

@ -22,6 +22,7 @@ import {
warn,
} from "../shared/util.js";
import { makePathFromDrawOPS } from "./display_utils.js";
import { serializeFontFamily } from "../shared/css_utils.js";
class FontLoader {
#systemFonts = new Set();
@ -439,7 +440,7 @@ class FontFaceObject {
css.style = `oblique ${this.cssFontInfo.italicAngle}deg`;
}
nativeFontFace = new FontFace(
this.cssFontInfo.fontFamily,
serializeFontFamily(this.cssFontInfo.fontFamily),
this.data,
css
);
@ -463,7 +464,10 @@ class FontFaceObject {
if (this.cssFontInfo.italicAngle) {
css += `font-style: oblique ${this.cssFontInfo.italicAngle}deg;`;
}
rule = `@font-face {font-family:"${this.cssFontInfo.fontFamily}";${css}src:${url}}`;
// The font family originates from the PDF document, hence it must be
// serialized as a <string> to prevent arbitrary rule injection.
const fontFamily = serializeFontFamily(this.cssFontInfo.fontFamily);
rule = `@font-face {font-family:${fontFamily};${css}src:${url}}`;
}
this._inspectFont?.(this, url);

View File

@ -25,6 +25,7 @@ import {
ensureResponseOrigin,
extractFilenameFromHeader,
getResponseOrigin,
trimHeadersEnd,
validateRangeRequestCapabilities,
} from "./network_utils.js";
import { endRequests } from "./transport_stream.js";
@ -208,9 +209,7 @@ class PDFNetworkStreamReader extends BasePDFStreamReader {
const rawResponseHeaders = fullRequestXhr.getAllResponseHeaders();
const responseHeaders = new Headers(
rawResponseHeaders
? rawResponseHeaders
.trimStart()
.replace(/[^\S ]+$/, "") // Not `trimEnd`, to keep regular spaces.
? trimHeadersEnd(rawResponseHeaders.trimStart())
.split(/[\r\n]+/)
.map(x => {
const [key, ...val] = x.split(": ");

View File

@ -32,6 +32,17 @@ function createHeaders(isHttp, httpHeaders) {
return headers;
}
// Trim the trailing whitespace of the raw response headers, but keep the
// regular spaces (hence no `trimEnd`). Scanning backwards keeps this linear,
// whereas a `$`-anchored regex is quadratic in the length of the run.
function trimHeadersEnd(str) {
let end = str.length;
while (end > 0 && str[end - 1] !== " " && /\s/.test(str[end - 1])) {
end--;
}
return str.slice(0, end);
}
function getResponseOrigin(url) {
// Notably, null is distinct from "null" string (e.g. from file:-URLs).
return URL.parse(url)?.origin ?? null;
@ -117,5 +128,6 @@ export {
ensureResponseOrigin,
extractFilenameFromHeader,
getResponseOrigin,
trimHeadersEnd,
validateRangeRequestCapabilities,
};

View File

@ -15,6 +15,10 @@
import { OutputScale, stopEvent } from "./display_utils.js";
function preventDefault(evt) {
evt.preventDefault();
}
class TouchManager {
#container;
@ -135,8 +139,13 @@ class TouchManager {
opt.capture = true;
container.addEventListener("pointerdown", stopEvent, opt);
container.addEventListener("pointermove", stopEvent, opt);
container.addEventListener("pointercancel", stopEvent, opt);
container.addEventListener("pointerup", stopEvent, opt);
// `pointerup` and `pointercancel` are only default-prevented: a
// `stopPropagation` in the capture phase also skips the bubble-phase
// listeners of the very node it's called on, hence swallowing them here
// would prevent any session in flight, e.g. an editor being resized, from
// ever being ended.
container.addEventListener("pointercancel", preventDefault, opt);
container.addEventListener("pointerup", preventDefault, opt);
this.#onPinchStart?.();
}
@ -189,7 +198,7 @@ class TouchManager {
const pDistance = Math.hypot(prevGapX, prevGapY) || 1;
if (
!this.#isPinching &&
Math.abs(pDistance - distance) <= TouchManager.MIN_TOUCH_DISTANCE_TO_PINCH
Math.abs(pDistance - distance) <= this.MIN_TOUCH_DISTANCE_TO_PINCH
) {
return;
}
@ -207,7 +216,12 @@ class TouchManager {
return;
}
const origin = [(screen0X + screen1X) / 2, (screen0Y + screen1Y) / 2];
// The distances are in screen CSS pixels, but the origin must be in client
// coordinates, like the one coming from a wheel event.
const origin = [
(touch0.clientX + touch1.clientX) / 2,
(touch0.clientY + touch1.clientY) / 2,
];
this.#onPinching?.(origin, pDistance, distance);
}

View File

@ -130,18 +130,12 @@ export class SandboxSupportBase {
}
this.win.alert(cMsg);
},
confirm: cMsg => {
if (typeof cMsg !== "string") {
return false;
}
return this.win.confirm(cMsg);
},
prompt: (cQuestion, cDefault) => {
if (typeof cQuestion !== "string" || typeof cDefault !== "string") {
return null;
}
return this.win.prompt(cQuestion, cDefault);
},
confirm: cMsg =>
typeof cMsg !== "string" ? false : this.win.confirm(cMsg),
prompt: (cQuestion, cDefault) =>
typeof cQuestion !== "string" || typeof cDefault !== "string"
? null
: this.win.prompt(cQuestion, cDefault),
parseURL: cUrl => {
const url = new this.win.URL(cUrl);
const props = [

View File

@ -19,7 +19,9 @@ class SandboxSupport extends SandboxSupportBase {
exportValueToSandbox(val) {
// The communication with the Quickjs sandbox is based on strings
// So we use JSON.stringfy to serialize
return JSON.stringify(val);
return JSON.stringify(val, (k, v) =>
v instanceof Map ? Object.fromEntries(v) : v
);
}
importValueFromSandbox(val) {
@ -65,7 +67,7 @@ class Sandbox {
let success = false;
let buf = 0;
try {
const sandboxData = JSON.stringify(data);
const sandboxData = this.support.exportValueToSandbox(data);
// "pdfjsScripting.initSandbox..." MUST be the last line to be evaluated
// since the returned value is used for the communication.
code.push(`pdfjsScripting.initSandbox({ data: ${sandboxData} })`);

View File

@ -52,11 +52,9 @@ class AForm {
}
AFMergeChange(event = globalThis.event) {
if (event.willCommit) {
return event.value.toString();
}
return this._app._eventDispatcher.mergeChange(event);
return event.willCommit
? event.value.toString()
: this._app._eventDispatcher.mergeChange(event);
}
AFParseDateEx(cString, cOrder) {
@ -102,10 +100,7 @@ class AForm {
}
AFMakeArrayFromList(string) {
if (typeof string === "string") {
return string.split(/, ?/g);
}
return string;
return typeof string === "string" ? string.split(/, ?/g) : string;
}
AFNumber_Format(
@ -616,11 +611,9 @@ class AForm {
}
AFExactMatch(rePatterns, str) {
if (rePatterns instanceof RegExp) {
return str.match(rePatterns)?.[0] === str || 0;
}
return rePatterns.findIndex(re => str.match(re)?.[0] === str) + 1;
return rePatterns instanceof RegExp
? str.match(rePatterns)?.[0] === str || 0
: rePatterns.findIndex(re => str.match(re)?.[0] === str) + 1;
}
}

View File

@ -21,8 +21,8 @@ const FieldType = {
time: 4,
};
function createActionsMap(actions) {
return new Map(actions ? Object.entries(actions) : null);
function createMap(val) {
return val instanceof Map ? val : new Map(val ? Object.entries(val) : null);
}
function getFieldType(actions) {
@ -30,10 +30,8 @@ function getFieldType(actions) {
if (!format) {
return FieldType.none;
}
format = format[0].trim();
format = format[0];
format = format.trim();
if (format.startsWith("AFNumber_")) {
return FieldType.number;
}
@ -49,4 +47,4 @@ function getFieldType(actions) {
return FieldType.none;
}
export { createActionsMap, FieldType, getFieldType };
export { createMap, FieldType, getFieldType };

View File

@ -14,7 +14,7 @@
*/
import { makeArr, makeMap, serializeError } from "./app_utils.js";
import { createActionsMap } from "./common.js";
import { createMap } from "./common.js";
import { PDFObject } from "./pdf_object.js";
import { PrintParams } from "./print_params.js";
import { ZoomType } from "./constants.js";
@ -98,7 +98,7 @@ class Doc extends PDFObject {
this._zoomType = ZoomType.none;
this._zoom = data.zoom || 100;
this._actions = createActionsMap(data.actions);
this._actions = createMap(data.actions);
this._globalEval = data.globalEval;
this._userActivation = false;
this._disablePrinting = false;
@ -173,9 +173,9 @@ class Doc extends PDFObject {
_dispatchPageEvent(name, actions, pageNumber) {
if (name === "PageOpen") {
this.#pageActions ??= new Map();
if (!this.#pageActions.has(pageNumber)) {
this.#pageActions.set(pageNumber, createActionsMap(actions));
}
this.#pageActions.getOrInsertComputed(pageNumber, () =>
createMap(actions)
);
this._pageNum = pageNumber - 1;
}

View File

@ -13,10 +13,10 @@
* limitations under the License.
*/
import { createActionsMap, FieldType, getFieldType } from "./common.js";
import { createMap, FieldType, getFieldType } from "./common.js";
import { makeArr, serializeError } from "./app_utils.js";
import { Color } from "./color.js";
import { PDFObject } from "./pdf_object.js";
import { serializeError } from "./app_utils.js";
class Field extends PDFObject {
constructor(data) {
@ -65,7 +65,7 @@ class Field extends PDFObject {
this.userName = data.userName;
// Private
this._actions = createActionsMap(data.actions);
this._actions = createMap(data.actions);
this._browseForFileToSubmit = data.browseForFileToSubmit || null;
this._buttonCaption = null;
this._buttonIcon = null;
@ -98,10 +98,7 @@ class Field extends PDFObject {
}
get currentValueIndices() {
if (!this._isChoice) {
return 0;
}
return this._currentValueIndices;
return !this._isChoice ? 0 : this._currentValueIndices;
}
set currentValueIndices(indices) {
@ -499,10 +496,7 @@ class Field extends PDFObject {
if (typeof cTrigger !== "string" || typeof cScript !== "string") {
return;
}
if (!(cTrigger in this._actions)) {
this._actions[cTrigger] = [];
}
this._actions[cTrigger].push(cScript);
this._actions.getOrInsertComputed(cTrigger, makeArr).push(cScript);
}
setFocus() {
@ -584,7 +578,7 @@ class RadioButtonField extends Field {
for (const radioData of otherButtons) {
this.exportValues.push(radioData.exportValues);
this._radioIds.push(radioData.id);
this._radioActions.push(createActionsMap(radioData.actions));
this._radioActions.push(createMap(radioData.actions));
if (this._value === radioData.exportValues) {
this._id = radioData.id;
}
@ -683,17 +677,13 @@ class CheckboxField extends RadioButtonField {
}
isBoxChecked(nWidget) {
if (this._value === "Off") {
return false;
}
return super.isBoxChecked(nWidget);
return this._value === "Off" ? false : super.isBoxChecked(nWidget);
}
isDefaultChecked(nWidget) {
if (this.defaultValue === "Off") {
return this._value === "Off";
}
return super.isDefaultChecked(nWidget);
return this.defaultValue === "Off"
? this._value === "Off"
: super.isDefaultChecked(nWidget);
}
checkThisBox(nWidget, bCheckIt = true) {

View File

@ -32,6 +32,7 @@ import { AForm } from "./aform.js";
import { App } from "./app.js";
import { Color } from "./color.js";
import { Console } from "./console.js";
import { createMap } from "./common.js";
import { Doc } from "./doc.js";
import { ProxyHandler } from "./proxy.js";
import { serializeError } from "./app_utils.js";
@ -70,11 +71,8 @@ function initSandbox(params) {
const util = new Util({ externalCall });
const appObjects = app._objects;
if (data.objects) {
for (const [name, objs] of createMap(data.objects)) {
const annotations = [];
for (const [name, objs] of Object.entries(data.objects)) {
annotations.length = 0;
let container = null;
for (const obj of objs) {
@ -126,7 +124,6 @@ function initSandbox(params) {
appObjects[container.id] = _object;
}
}
}
const color = new Color();

View File

@ -43,13 +43,10 @@ class ProxyHandler {
}
set(obj, prop, value) {
if (obj._kidIds) {
// If the field is a container for other fields then
// dispatch the kids.
obj._kidIds.forEach(id => {
// If the field is a container for other fields then dispatch the kids.
obj._kidIds?.forEach(id => {
obj._appObjects[id].wrapped[prop] = value;
});
}
if (typeof prop === "string" && !prop.startsWith("_") && prop in obj) {
const old = obj[prop];

View File

@ -252,10 +252,9 @@ class Util extends PDFObject {
const patterns =
/(mmmm|mmm|mm|m|dddd|ddd|dd|d|yyyy|yy|HH|H|hh|h|MM|M|ss|s|tt|t|\\.)/g;
return cFormat.replaceAll(patterns, function (match, pattern) {
if (pattern in handlers) {
return handlers[pattern](data);
}
return pattern.charCodeAt(1);
return pattern in handlers
? handlers[pattern](data)
: pattern.charCodeAt(1);
});
}

76
src/shared/css_utils.js Normal file
View File

@ -0,0 +1,76 @@
/* Copyright 2026 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const CONTROL_CHAR_REGEXP = /\p{Cc}/u;
/**
* Checks if the given value already is a well-formed CSS <string>, i.e. a
* value which can be used verbatim since it cannot introduce delimiters.
* See https://drafts.csswg.org/css-syntax/#string-token-diagram.
* @param {string} str
* @returns {boolean}
*/
function isCSSString(str) {
const quote = str[0];
if (
str.length < 2 ||
(quote !== `"` && quote !== `'`) ||
str.at(-1) !== quote
) {
return false;
}
const end = str.length - 1;
for (let i = 1; i < end; i++) {
const char = str[i];
if (char === quote || CONTROL_CHAR_REGEXP.test(char)) {
return false;
}
if (char === "\\") {
// Skip the escaped character. A trailing backslash would instead escape
// the closing quote, and control characters must not occur in a CSS
// <string> even when escaped this way.
if (++i >= end || CONTROL_CHAR_REGEXP.test(str[i])) {
return false;
}
}
}
return true;
}
/**
* Serializes a font family, originating from the PDF document, such that it
* can safely be interpolated into CSS.
* @param {string} fontFamily
* @returns {string}
*/
function serializeFontFamily(fontFamily) {
if (isCSSString(fontFamily)) {
return fontFamily;
}
// Always emit a <string>, rather than a <custom-ident> sequence, since both
// denote the same family name but only the former cannot be mistaken for a
// generic family (e.g. `serif`) or a CSS-wide keyword (e.g. `inherit`);
// those are not valid font family names and would be ignored.
// Control characters use hexadecimal escapes, since CSS line terminators
// cannot be escaped by simply prefixing them with a backslash.
const escaped = fontFamily.replaceAll(/["\\\p{Cc}]/gu, char =>
char === `"` || char === "\\"
? `\\${char}`
: `\\${char.codePointAt(0).toString(16)} `
);
return `"${escaped}"`;
}
export { CONTROL_CHAR_REGEXP, serializeFontFamily };

View File

@ -1007,10 +1007,9 @@ class Driver {
}
_getLastPageNumber(task) {
if (!task.pdfDoc) {
return task.firstPage || 1;
}
return task.lastPage || task.pdfDoc.numPages;
return !task.pdfDoc
? task.firstPage || 1
: task.lastPage || task.pdfDoc.numPages;
}
_nextPage(task, loadError) {

View File

@ -31,10 +31,12 @@ import {
kbUndo,
loadAndWait,
moveEditor,
pinch,
scrollIntoView,
selectEditor,
selectEditors,
switchToEditor,
unselectEditor,
waitForAnnotationModeChanged,
waitForNoElement,
waitForPointerUp,
@ -65,6 +67,26 @@ const drawLine = async (page, x0, y0, x1, y1) => {
await awaitPromise(clickHandle);
};
// Draw an editor large enough to have room for two fingers on it, and leave it
// selected since that's what makes it resizable with a touchscreen.
const drawAndSelectEditor = async page => {
await switchToInk(page);
const { x, y, width, height } = await getRect(page, ".annotationEditorLayer");
await drawLine(
page,
x + 0.15 * width,
y + 0.1 * height,
x + 0.75 * width,
y + 0.35 * height
);
await commit(page);
return {
layer: { x, y, width, height },
editor: await getRect(page, getEditorSelector(0)),
};
};
describe("Ink Editor", () => {
describe("Basic operations", () => {
let pages;
@ -1407,3 +1429,148 @@ describe("Ink must be committed when the document is saved", () => {
);
});
});
describe("Pinch to resize a drawing", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must keep resizing a drawing which came back with an undo", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToInk(page);
const rect = await getRect(page, ".annotationEditorLayer");
await drawLine(
page,
rect.x + 200,
rect.y + 200,
rect.x + 300,
rect.y + 260
);
await commit(page);
const editorSelector = getEditorSelector(0);
await clearAll(page);
await waitForNoElement(page, editorSelector);
await kbUndo(page);
await page.waitForSelector(editorSelector);
await selectEditor(page, editorSelector);
const before = await getRect(page, editorSelector);
const startGap = Math.min(before.width, before.height) * 0.2;
const endGap = Math.max(before.width, before.height) * 1.8;
await pinch(page, {
centerX: before.x + before.width / 2,
centerY: before.y + before.height / 2,
startGap,
endGap,
});
const { width: after } = await getRect(page, editorSelector);
expect(after)
.withContext(`In ${browserName}`)
.toBeGreaterThan(before.width);
})
);
});
});
describe("Resize with a touchscreen", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the resize session is ended when the finger is lifted while another one is down", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const { layer } = await drawAndSelectEditor(page);
// Grabbing a resizer starts a resize session, which disables the
// pointer events of the editor layer until the finger is lifted.
const resizer = await getRect(
page,
`${getEditorSelector(0)} .resizer.bottomRight`
);
await pinch(page, {
steps: 0,
startPoints: [
{
x: resizer.x + resizer.width / 2,
y: resizer.y + resizer.height / 2,
},
{
x: layer.x + 0.3 * layer.width,
y: layer.y + 0.9 * layer.height,
},
],
afterFirstStart: () =>
page.waitForSelector(".annotationEditorLayer.disabled"),
afterFirstEnd: () =>
// The `pointerup` of the resizing finger must still be dispatched,
// else the resize session would never end and the editor would keep
// being resized by the plain mouse moves coming afterwards.
page.waitForSelector(".annotationEditorLayer:not(.disabled)"),
});
})
);
});
});
describe("Tap after a two-finger gesture", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the tap following a two-finger gesture selects the editor", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const { layer, editor } = await drawAndSelectEditor(page);
const editorSelector = getEditorSelector(0);
// One finger on the editor, which starts a drag session, and a second
// one elsewhere on the page, which turns it into a two-finger gesture.
await pinch(page, {
steps: 0,
startPoints: [
{
x: editor.x + 0.5 * editor.width,
y: editor.y + 0.8 * editor.height,
},
{
x: layer.x + 0.3 * layer.width,
y: layer.y + 0.9 * layer.height,
},
],
});
// Once every finger is up, no listener may be left behind to swallow
// the `pointerdown` of the next tap on the editor.
await unselectEditor(page, editorSelector);
await page.touchscreen.tap(
editor.x + 0.5 * editor.width,
editor.y + 0.8 * editor.height
);
await waitForSelectedEditor(page, editorSelector);
})
);
});
});

View File

@ -1204,6 +1204,69 @@ describe("Interaction", () => {
);
});
it("must efficiently delete a word from a large field", async () => {
const nonWordLength = 200000;
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForScripting(page);
const result = await page.$eval(
getSelector("27R"),
(element, length) => {
element.value = `${"!".repeat(length)}a`;
element.setSelectionRange(
element.value.length,
element.value.length
);
const eventBus = window.PDFViewerApplication.eventBus;
const eventBusPrototype = Object.getPrototypeOf(eventBus);
const originalDispatch = eventBusPrototype.dispatch;
let selection;
eventBusPrototype.dispatch = function (eventName, data) {
if (
this === eventBus &&
eventName === "dispatcheventinsandbox"
) {
const { selEnd, selStart } = data.detail;
selection = { selEnd, selStart };
return;
}
originalDispatch.call(this, eventName, data);
};
const event = new InputEvent("beforeinput", {
bubbles: true,
cancelable: true,
inputType: "deleteWordBackward",
});
try {
const startTime = performance.now();
element.dispatchEvent(event);
return {
duration: performance.now() - startTime,
selection,
};
} finally {
eventBusPrototype.dispatch = originalDispatch;
}
},
nonWordLength
);
expect(result.selection)
.withContext(`In ${browserName}`)
.toEqual({
selEnd: nonWordLength + 1,
selStart: nonWordLength,
});
expect(result.duration)
.withContext(`In ${browserName}`)
.toBeLessThan(1000);
})
);
});
it("must check that an infinite loop is not triggered", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {

View File

@ -600,6 +600,77 @@ async function dragAndDrop(page, selector, translations, steps = 1) {
await page.waitForSelector("#viewer:not(.noUserSelect)");
}
// Move two fingers, horizontally centered on (centerX, centerY), from startGap
// to endGap: it's a pinch out when endGap is larger than startGap.
// Keep in mind that `TouchManager` starts to pinch only once the distance
// between the two fingers changed by more than `MIN_TOUCH_DISTANCE_TO_PINCH`
// (35 CSS pixels), hence the first moves are swallowed and the resulting zoom
// factor is smaller than endGap / startGap.
// Explicit start/end points can be used for tests which need an asymmetric
// gesture, or hooks around each touch lifetime.
async function pinch(
page,
{
afterEnd = null,
afterFirstEnd = null,
afterFirstStart = null,
afterStart = null,
beforeEnd = null,
centerX = 0,
centerY = 0,
centerDeltaX = 0,
centerDeltaY = 0,
startGap = 0,
endGap = startGap,
endPoints = null,
startPoints = null,
steps = 12,
}
) {
const normalizePoint = point =>
Array.isArray(point) ? { x: point[0], y: point[1] } : point;
const start = (
startPoints || [
{ x: centerX - startGap, y: centerY },
{ x: centerX + startGap, y: centerY },
]
).map(normalizePoint);
let end;
if (endPoints) {
end = endPoints.map(normalizePoint);
} else if (startPoints) {
end = start;
} else {
end = [
{ x: centerX + centerDeltaX - endGap, y: centerY + centerDeltaY },
{ x: centerX + centerDeltaX + endGap, y: centerY + centerDeltaY },
];
}
const finger0 = await page.touchscreen.touchStart(start[0].x, start[0].y);
await afterFirstStart?.(finger0);
const finger1 = await page.touchscreen.touchStart(start[1].x, start[1].y);
await afterStart?.([finger0, finger1]);
for (let i = 1; i <= steps; i++) {
const t = i / steps;
await finger0.move(
start[0].x + (end[0].x - start[0].x) * t,
start[0].y + (end[0].y - start[0].y) * t
);
await finger1.move(
start[1].x + (end[1].x - start[1].x) * t,
start[1].y + (end[1].y - start[1].y) * t
);
}
await beforeEnd?.([finger0, finger1]);
await finger0.end();
await afterFirstEnd?.([finger0, finger1]);
await finger1.end();
await afterEnd?.([finger0, finger1]);
}
function waitForPageChanging(page) {
return createPromise(page, resolve => {
window.PDFViewerApplication.eventBus.on("pagechanging", resolve, {
@ -1163,6 +1234,7 @@ export {
paste,
pasteFromClipboard,
PDI,
pinch,
scrollIntoView,
selectEditor,
selectEditors,

View File

@ -20,9 +20,11 @@
import {
closePages,
closeSinglePage,
firstPageOnTop,
getSpanRectFromText,
kbSelectAll,
loadAndWait,
scrollIntoView,
waitForEvent,
} from "./test_utils.mjs";
import { MathClamp } from "../../src/shared/math_clamp.js";
@ -1148,6 +1150,94 @@ describe("Text layer", () => {
);
});
});
describe("when the page has been destroyed and rendered again", () => {
const selectionSelector = ".canvasWrapper .selection svg path[d]";
const timeout = 5000;
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
`.page[data-page-number = "1"] .endOfContent`,
undefined,
undefined,
{ annotationEditorMode: -1 }
);
});
afterEach(async () => {
await closePages(pages);
});
async function selectSomeText(page) {
const [positionStart, positionEnd] = await Promise.all([
getSpanRectFromText(
page,
1,
"(frequently executed) bytecode sequences, records"
).then(middlePosition),
getSpanRectFromText(
page,
1,
"them, and compiles them to fast native code. We call such a se-"
).then(belowEndPosition),
]);
await page.mouse.move(positionStart.x, positionStart.y);
await page.mouse.down();
await moveInSteps(page, positionStart, positionEnd, 20);
await page.mouse.up();
}
it("must draw the selection", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await selectSomeText(page);
await page.waitForSelector(selectionSelector, { timeout });
await page.evaluate(() => {
document.getSelection().removeAllRanges();
});
await page.waitForSelector(selectionSelector, {
hidden: true,
timeout,
});
// Scroll down, page by page, until the first page view is
// evicted from the buffer: its canvas wrapper is then removed.
const pagesCount = await page.evaluate(
() => window.PDFViewerApplication.pagesCount
);
let isDestroyed = false;
for (let i = 2; i <= pagesCount && !isDestroyed; i++) {
const selector = `.page[data-page-number = "${i}"]`;
await scrollIntoView(page, selector);
await page.waitForSelector(
`${selector} .canvasWrapper canvas`,
{ timeout: 0 }
);
isDestroyed = !(await page.$(
`.page[data-page-number = "1"] .canvasWrapper`
));
}
expect(isDestroyed)
.withContext(`In ${browserName}, first page destroyed`)
.toBeTrue();
await firstPageOnTop(page);
await page.waitForSelector(
`.page[data-page-number = "1"] .canvasWrapper canvas`,
{ timeout: 0 }
);
await selectSomeText(page);
await page.waitForSelector(selectionSelector, { timeout });
})
);
});
});
});
describe("using selection carets", () => {

View File

@ -20,6 +20,7 @@ import {
getRect,
getSpanRectFromText,
loadAndWait,
pinch,
scrollIntoView,
showViewsManager,
waitAndClick,
@ -1527,7 +1528,11 @@ describe("PDF viewer", () => {
beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
`.page[data-page-number = "1"] .endOfContent`
`.page[data-page-number = "1"] .endOfContent`,
// Pin the zoom: the drift checked below is proportional to the zoom
// level reached at the end of the pinch, and the default `page-fit`
// depends on the size of the window.
50
);
});
@ -1535,20 +1540,9 @@ describe("PDF viewer", () => {
await closePages(pages);
});
it("keeps the content under the pinch centre fixed on the screen", async () => {
it("keeps the content under the pinch center fixed on the screen", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
if (browserName === "firefox") {
pending(
"Touch events are not supported on devices without touch screen in Firefox."
);
}
if (browserName === "chrome") {
pending(
"Pinch zoom emulation is not supported for WebDriver BiDi in Chrome."
);
}
const rect = await getSpanRectFromText(page, 1, "type-stable");
const originX = rect.x + rect.width / 2;
const originY = rect.y + rect.height / 2;
@ -1564,14 +1558,17 @@ describe("PDF viewer", () => {
};
window.PDFViewerApplication.eventBus.on("textlayerrendered", cb);
});
const client = await page.target().createCDPSession();
await client.send("Input.synthesizePinchGesture", {
x: originX,
y: originY,
scaleFactor: 3,
gestureSourceType: "touch",
// Spread the two fingers from 50 to 200 pixels apart: the first
// moves are swallowed until the distance between them changed by
// more than 35 pixels, hence a zoom factor of about 200/85 = 2.4.
await pinch(page, {
centerX: originX,
centerY: originY,
startGap: 25,
endGap: 100,
});
await awaitPromise(rendered);
const spanHandle = await page.evaluateHandle(() =>
Array.from(
document.querySelectorAll(
@ -1579,7 +1576,21 @@ describe("PDF viewer", () => {
)
).find(span => span.textContent.includes("type-stable"))
);
expect(await spanHandle.isIntersectingViewport()).toBeTrue();
expect(await spanHandle.isIntersectingViewport())
.withContext(`In ${browserName}`)
.toBeTrue();
// The text which was under the fingers must still be at the same
// height: only vertically because a page which is larger than its
// container isn't centered in it anymore.
// A few pixels are tolerated because the origin is preserved by
// scrolling: Chrome snaps the scroll offsets to the device pixels and
// the discarded fractions show up as a small drift. It's exact in
// Firefox, which keeps them.
const newRect = await getSpanRectFromText(page, 1, "type-stable");
expect(Math.abs(newRect.y + newRect.height / 2 - originY))
.withContext(`In ${browserName}`)
.toBeLessThan(5);
})
);
});

View File

@ -2460,17 +2460,14 @@ describe("annotation", function () {
annotationGlobalsMock,
idFactoryMock
);
const fieldObject = await annotation.getFieldObject();
const actions = fieldObject.actions;
expect(actions["Mouse Enter"]).toEqual(["hello()"]);
expect(actions["Mouse Exit"]).toEqual([
"world()",
"olleh()",
"foo()",
"dlrow()",
"oof()",
]);
expect(actions["Mouse Down"]).toEqual(["bar()"]);
const { actions } = await annotation.getFieldObject();
expect(actions).toEqual(
new Map([
["Mouse Enter", ["hello()"]],
["Mouse Exit", ["world()", "olleh()", "foo()", "dlrow()", "oof()"]],
["Mouse Down", ["bar()"]],
])
);
});
it("should save Japanese text", async function () {
@ -3728,7 +3725,7 @@ describe("annotation", function () {
);
expect(data.annotationType).toEqual(AnnotationType.WIDGET);
expect(data.pushButton).toBeTrue();
expect(data.actions.Action).toEqual(["do_something();"]);
expect(data.actions.get("Action")).toEqual(["do_something();"]);
});
it("should handle push buttons that act as a tooltip only", async function () {

View File

@ -986,9 +986,9 @@ describe("api", function () {
expect(pdfDocument.numPages).toEqual(1);
const jsActions = await pdfDocument.getJSActions();
expect(jsActions).toEqual({
OpenAction: ["func=function(){app.alert(1)};func();"],
});
expect(jsActions).toEqual(
new Map([["OpenAction", ["func=function(){app.alert(1)};func();"]]])
);
const page = await pdfDocument.getPage(1);
expect(page).toBeInstanceOf(PDFPageProxy);
@ -1943,12 +1943,17 @@ describe("api", function () {
// PDF document with "JavaScript" action in the OpenAction dictionary.
const loadingTask = getDocument(buildGetDocumentParams("issue6106.pdf"));
const pdfDoc = await loadingTask.promise;
const { OpenAction } = await pdfDoc.getJSActions();
const jsActions = await pdfDoc.getJSActions();
expect(OpenAction).toEqual([
"this.print({bUI:true,bSilent:false,bShrinkToFit:true});",
]);
expect(OpenAction[0]).toMatch(AutoPrintRegExp);
expect(jsActions).toEqual(
new Map([
[
"OpenAction",
["this.print({bUI:true,bSilent:false,bShrinkToFit:true});"],
],
])
);
expect(jsActions.get("OpenAction")[0]).toMatch(AutoPrintRegExp);
await loadingTask.destroy();
});
@ -1988,21 +1993,27 @@ describe("api", function () {
const page3 = await pdfDoc.getPage(3);
const page3Actions = await page3.getJSActions();
expect(docActions).toEqual({
DidPrint: [`this.getField("Text2").value = "DidPrint";`],
DidSave: [`this.getField("Text2").value = "DidSave";`],
WillClose: [`this.getField("Text1").value = "WillClose";`],
WillPrint: [`this.getField("Text1").value = "WillPrint";`],
WillSave: [`this.getField("Text1").value = "WillSave";`],
});
expect(page1Actions).toEqual({
PageOpen: [`this.getField("Text1").value = "PageOpen 1";`],
PageClose: [`this.getField("Text2").value = "PageClose 1";`],
});
expect(page3Actions).toEqual({
PageOpen: [`this.getField("Text5").value = "PageOpen 3";`],
PageClose: [`this.getField("Text6").value = "PageClose 3";`],
});
expect(docActions).toEqual(
new Map([
["DidPrint", [`this.getField("Text2").value = "DidPrint";`]],
["DidSave", [`this.getField("Text2").value = "DidSave";`]],
["WillClose", [`this.getField("Text1").value = "WillClose";`]],
["WillPrint", [`this.getField("Text1").value = "WillPrint";`]],
["WillSave", [`this.getField("Text1").value = "WillSave";`]],
])
);
expect(page1Actions).toEqual(
new Map([
["PageOpen", [`this.getField("Text1").value = "PageOpen 1";`]],
["PageClose", [`this.getField("Text2").value = "PageClose 1";`]],
])
);
expect(page3Actions).toEqual(
new Map([
["PageOpen", [`this.getField("Text5").value = "PageOpen 3";`]],
["PageClose", [`this.getField("Text6").value = "PageClose 3";`]],
])
);
await loadingTask.destroy();
});
@ -2017,8 +2028,11 @@ describe("api", function () {
const pdfDoc = await loadingTask.promise;
const fieldObjects = await pdfDoc.getFieldObjects();
expect(fieldObjects).toEqual({
Text1: [
expect(fieldObjects).toEqual(
new Map([
[
"Text1",
[
{
id: "25R",
value: "",
@ -2041,7 +2055,10 @@ describe("api", function () {
type: "text",
},
],
Button1: [
],
[
"Button1",
[
{
id: "26R",
value: "Off",
@ -2051,11 +2068,14 @@ describe("api", function () {
name: "Button1",
rect: [455.436, 719.678, 527.436, 739.678],
hidden: false,
actions: {
Action: [
actions: new Map([
[
"Action",
[
`this.getField("Text1").value = this.info.authors.join("::");`,
],
},
],
]),
page: 0,
strokeColor: null,
fillColor: new Uint8ClampedArray([192, 192, 192]),
@ -2063,7 +2083,9 @@ describe("api", function () {
type: "button",
},
],
});
],
])
);
await loadingTask.destroy();
});
@ -2073,8 +2095,8 @@ describe("api", function () {
const pdfDoc = await loadingTask.promise;
const fieldObjects = await pdfDoc.getFieldObjects();
for (const name in fieldObjects) {
const pageIndexes = fieldObjects[name].map(o => o.page);
for (const [name, objs] of fieldObjects) {
const pageIndexes = objs.map(o => o.page);
let expected;
switch (name) {
@ -6286,6 +6308,29 @@ small scripts as well as for`);
await loadingTask.destroy();
});
it("clones shared indirect objects reached concurrently only once", async function () {
const pdfData = assemblePdf([
"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n",
"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
"3 0 obj\n<< /Type /Page /Parent 2 0 R " +
"/MediaBox [0 0 100 100] /Resources << /ExtGState " +
"<< /GS1 4 0 R /GS2 4 0 R >> >> >>\nendobj\n",
"4 0 obj\n<< /Type /ExtGState /CA 0.5 >>\nendobj\n",
]);
const loadingTask = getDocument({ data: pdfData });
const pdfDoc = await loadingTask.promise;
const data = await pdfDoc.extractPages([{ document: null }]);
expect(countMarker(data, "/Type /ExtGState")).toEqual(1);
await loadingTask.destroy();
const newLoadingTask = getDocument({ data });
const newPdfDoc = await newLoadingTask.promise;
expect(newPdfDoc.numPages).toEqual(1);
await newPdfDoc.getPage(1);
await newLoadingTask.destroy();
});
it("should merge two PDFs with page included ranges", async function () {
const loadingTask = getDocument(
buildGetDocumentParams("tracemonkey.pdf")
@ -7671,6 +7716,36 @@ small scripts as well as for`);
return fontIndex < 0 ? null : operatorList.argsArray[fontIndex][0];
};
it("does not mutate source widget parents", async function () {
const pdfData = assemblePdf([
"1 0 obj\n<< /Type /Catalog /Pages 2 0 R " +
"/AcroForm 6 0 R >>\nendobj\n",
"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
"3 0 obj\n<< /Type /Page /Parent 2 0 R " +
"/MediaBox [0 0 100 100] /Annots [4 0 R] >>\nendobj\n",
"4 0 obj\n<< /Type /Annot /Subtype /Widget /Rect [0 0 20 10] " +
"/Parent 5 0 R >>\nendobj\n",
"5 0 obj\n<< /FT /Tx /T (group) /Kids [4 0 R] >>\nendobj\n",
"6 0 obj\n<< /Fields [] /DA (/Helv 10 Tf) >>\nendobj\n",
]);
let loadingTask = getDocument({ data: pdfData });
let pdfDoc = await loadingTask.promise;
const data = await pdfDoc.extractPages([{ document: null }]);
const sourceAnnotations = await (
await pdfDoc.getPage(1)
).getAnnotations();
expect(sourceAnnotations[0].fieldName).toEqual("group");
expect(sourceAnnotations[0].fieldType).toEqual("Tx");
await loadingTask.destroy();
loadingTask = getDocument({ data });
pdfDoc = await loadingTask.promise;
expect(pdfDoc.numPages).toEqual(1);
await loadingTask.destroy();
});
it("rebuilds a missing AcroForm Fields array", async function () {
const data = assemblePdf([
"1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 6 0 R >>\nendobj\n",
@ -7691,9 +7766,12 @@ small scripts as well as for`);
loadingTask = getDocument({ data: extracted });
pdfDoc = await loadingTask.promise;
expect(Object.keys(await pdfDoc.getFieldObjects())).toEqual(["group"]);
const fieldObjects = await pdfDoc.getFieldObjects();
expect([...fieldObjects.keys()]).toEqual(["group"]);
const annotations = await (await pdfDoc.getPage(1)).getAnnotations();
expect(annotations[0].fieldName).toEqual("group");
await loadingTask.destroy();
});
@ -7721,7 +7799,10 @@ small scripts as well as for`);
loadingTask = getDocument({ data });
pdfDoc = await loadingTask.promise;
expect(Object.keys(await pdfDoc.getFieldObjects())).toEqual(["field"]);
const fieldObjects = await pdfDoc.getFieldObjects();
expect([...fieldObjects.keys()]).toEqual(["field"]);
await loadingTask.destroy();
});
@ -7779,9 +7860,10 @@ small scripts as well as for`);
loadingTask = getDocument({ data });
pdfDoc = await loadingTask.promise;
expect(Object.keys(await pdfDoc.getFieldObjects())).toEqual([
"signature",
]);
const fieldObjects = await pdfDoc.getFieldObjects();
expect([...fieldObjects.keys()]).toEqual(["signature"]);
await loadingTask.destroy();
});
@ -7827,7 +7909,7 @@ small scripts as well as for`);
// reflects the T entries of the fields in the AcroForm dictionary.
const fieldObjects = await pdfDoc.getFieldObjects();
expect(fieldObjects).not.toBeNull();
expect(Object.keys(fieldObjects).sort()).toEqual(origFieldNames);
expect([...fieldObjects.keys()].sort()).toEqual(origFieldNames);
await loadingTask.destroy();
});
@ -7885,7 +7967,7 @@ small scripts as well as for`);
const allOrigFieldNames = [
...new Set([...origPage1FieldNames, ...origPage2FieldNames]),
].sort();
expect(Object.keys(fieldObjects).sort()).toEqual(allOrigFieldNames);
expect([...fieldObjects.keys()].sort()).toEqual(allOrigFieldNames);
await loadingTask.destroy();
});
@ -7897,9 +7979,8 @@ small scripts as well as for`);
let pdfDoc = await loadingTask.promise;
expect(await pdfDoc.getCalculationOrderIds()).toEqual(["6R"]);
expect(Object.keys((await pdfDoc.getFieldObjects()) || {})).toEqual([
"group",
]);
const fieldObjects1 = await pdfDoc.getFieldObjects();
expect([...fieldObjects1.keys()]).toEqual(["group"]);
const data = await pdfDoc.extractPages([{ document: null }]);
await loadingTask.destroy();
@ -7911,9 +7992,8 @@ small scripts as well as for`);
expect(Array.isArray(calculationOrder)).toBeTrue();
expect(calculationOrder.length).toEqual(1);
expect(calculationOrder[0]).not.toEqual("6R");
expect(Object.keys((await pdfDoc.getFieldObjects()) || {})).toEqual([
"group",
]);
const fieldObjects2 = await pdfDoc.getFieldObjects();
expect([...fieldObjects2.keys()]).toEqual(["group"]);
await loadingTask.destroy();
});
@ -7956,9 +8036,9 @@ small scripts as well as for`);
loadingTask = getDocument({ data });
pdfDoc = await loadingTask.promise;
expect(pdfDoc.numPages).toEqual(2);
expect(
Object.keys((await pdfDoc.getFieldObjects()) ?? {}).sort()
).toEqual(["first", "second"]);
const fieldObjects = await pdfDoc.getFieldObjects();
expect([...fieldObjects.keys()].sort()).toEqual(["first", "second"]);
for (const pageNumber of [1, 2]) {
const fontName = await getAppearanceFontName(pdfDoc, pageNumber);
expect(fontName).not.toBeNull();
@ -7997,9 +8077,9 @@ small scripts as well as for`);
loadingTask = getDocument({ data });
pdfDoc = await loadingTask.promise;
expect(pdfDoc.numPages).toEqual(2);
expect(
Object.keys((await pdfDoc.getFieldObjects()) ?? {}).sort()
).toEqual(["broken", "main"]);
const fieldObjects = await pdfDoc.getFieldObjects();
expect([...fieldObjects.keys()].sort()).toEqual(["broken", "main"]);
const fontName = await getAppearanceFontName(pdfDoc, 2);
expect(fontName).not.toBeNull();
expect(fontName).not.toEqual("g_font_error");
@ -8042,9 +8122,9 @@ small scripts as well as for`);
loadingTask = getDocument({ data });
pdfDoc = await loadingTask.promise;
expect(pdfDoc.numPages).toEqual(2);
expect(
Object.keys((await pdfDoc.getFieldObjects()) ?? {}).sort()
).toEqual(["check", "main"]);
const fieldObjects = await pdfDoc.getFieldObjects();
expect([...fieldObjects.keys()].sort()).toEqual(["check", "main"]);
const fontName = await getAppearanceFontName(pdfDoc, 2);
expect(fontName).not.toBeNull();
expect(fontName).not.toEqual("g_font_error");

View File

@ -220,4 +220,24 @@ describe("autolinker", function () {
["john.doe@uni-cityname.tld", "mailto:john.doe@uni-cityname.tld"],
]);
});
it("should find emails with the longest parts allowed by the RFCs", function () {
const local = "a".repeat(64);
const label = "b".repeat(63);
testLinks([[`${local}@${label}.com`, `mailto:${local}@${label}.com`]]);
});
it("shouldn't find emails with parts longer than allowed by the RFCs", function () {
expect(
Autolinker.findLinks(`${"a".repeat(107)}@${"a".repeat(80)}.com`)
).toEqual([]);
});
it("should handle a long run of characters before an @ efficiently", function () {
const text = `${"a".repeat(50000)}@`;
const startTime = performance.now();
expect(Autolinker.findLinks(text)).toEqual([]);
expect(performance.now() - startTime).toBeLessThan(1000);
});
});

View File

@ -26,6 +26,7 @@
"evaluator_spec.js",
"event_utils_spec.js",
"fetch_stream_spec.js",
"font_loader_spec.js",
"font_substitutions_spec.js",
"fonts_spec.js",
"image_utils_spec.js",

View File

@ -24,6 +24,7 @@ import {
getRotationMatrix,
getSizeInBytes,
isWhiteSpace,
normalizeCSSFontFamily,
numberToString,
parseXFAPath,
recoverJsURL,
@ -243,6 +244,32 @@ describe("core_utils", function () {
{ name: "BAR", pos: 456 },
]);
});
it("should ignore a malformed position", function () {
expect(parseXFAPath("foo[].bar[1x].oof[].[3]")).toEqual([
{ name: "foo[]", pos: 0 },
{ name: "bar[1x]", pos: 0 },
{ name: "oof[]", pos: 0 },
{ name: "[3]", pos: 0 },
]);
});
it("should keep the longest name when a component has several brackets", function () {
expect(parseXFAPath("foo[1][2]")).toEqual([{ name: "foo[1]", pos: 2 }]);
});
it("should handle a long component efficiently", function () {
// Looking for the position with a leading `.+` is quadratic in the
// length of a component which doesn't end with one.
const name = "a".repeat(200000);
const startTime = performance.now();
const parsedPath = parseXFAPath(name);
const duration = performance.now() - startTime;
expect(parsedPath).toEqual([{ name, pos: 0 }]);
expect(duration).toBeLessThan(1000);
});
});
describe("recoverJsURL", function () {
@ -325,6 +352,41 @@ describe("core_utils", function () {
});
});
describe("normalizeCSSFontFamily", function () {
it("should strip the spaces preceding a digit", function () {
expect(normalizeCSSFontFamily("Wingdings 3")).toEqual("Wingdings3");
expect(normalizeCSSFontFamily("Wingdings 3")).toEqual("Wingdings3");
expect(normalizeCSSFontFamily(" 1 2 3")).toEqual("123");
expect(normalizeCSSFontFamily("MS Gothic 2 Bold 7")).toEqual(
"MS Gothic2 Bold7"
);
});
it("should keep the spaces which don't precede a digit", function () {
expect(normalizeCSSFontFamily("")).toEqual("");
expect(normalizeCSSFontFamily("Times New Roman")).toEqual(
"Times New Roman"
);
// The runs of spaces must be preserved as-is.
expect(normalizeCSSFontFamily(" Times New Roman ")).toEqual(
" Times New Roman "
);
// A digit which isn't preceded by a space is left alone.
expect(normalizeCSSFontFamily("Wingdings3")).toEqual("Wingdings3");
});
it("should handle long runs of spaces efficiently", function () {
// Guard against a regular expression that backtracks over the spaces,
// which makes the replacement quadratic: that needs several seconds
// here, whereas a linear one needs well under a millisecond.
const fontFamily = `Wingdings${" ".repeat(100000)}`;
const startTime = performance.now();
expect(normalizeCSSFontFamily(fontFamily)).toEqual("Wingdings ");
expect(performance.now() - startTime).toBeLessThan(1000);
});
});
describe("validateCSSFont", function () {
it("Check font family", function () {
const cssFontInfo = {
@ -378,6 +440,30 @@ describe("core_utils", function () {
expect(validateCSSFont(cssFontInfo)).toBeFalse();
});
it("Check font family containing control characters", function () {
const cssFontInfo = {
fontFamily: "",
fontWeight: 0,
italicAngle: 0,
};
// A form feed is a newline in CSS, hence it terminates the <string>.
cssFontInfo.fontFamily = `"blah\fblah"`;
expect(validateCSSFont(cssFontInfo)).toBeFalse();
cssFontInfo.fontFamily = `"blah\x00blah"`;
expect(validateCSSFont(cssFontInfo)).toBeFalse();
cssFontInfo.fontFamily = `"blah\tblah"`;
expect(validateCSSFont(cssFontInfo)).toBeFalse();
cssFontInfo.fontFamily = `"blah\nblah"`;
expect(validateCSSFont(cssFontInfo)).toBeFalse();
cssFontInfo.fontFamily = `"blah blah"`;
expect(validateCSSFont(cssFontInfo)).toBeTrue();
});
it("Check font weight", function () {
const cssFontInfo = {
fontFamily: "blah",

View File

@ -122,6 +122,30 @@ describe("display_utils", function () {
expect(
getPdfFilenameFromUrl("http://www.example.com/pdfs/pdf.html#file2.pdf")
).toEqual("file2.pdf");
// Only the last ".pdf" of the hash is used.
expect(getPdfFilenameFromUrl("/pdfs/pdfs.html#a.pdf/b.pdf")).toEqual(
"b.pdf"
);
// A ".pdf" which isn't preceded by a name is ignored.
expect(getPdfFilenameFromUrl("/pdfs/pdfs.html#=.pdf")).toEqual(
"document.pdf"
);
// An invalid last ".pdf" prevents an earlier valid one from being used.
expect(getPdfFilenameFromUrl("/pdfs/pdfs.html#a.pdf/=.pdf")).toEqual(
"document.pdf"
);
});
it("gets PDF filename from a long hash string efficiently", function () {
// Scanning the hash for a name is quadratic when it contains no ".pdf".
const url = `/pdfs/pdfs.html#${"a".repeat(200000)}`;
const startTime = performance.now();
const filename = getPdfFilenameFromUrl(url);
const duration = performance.now() - startTime;
expect(filename).toEqual("document.pdf");
expect(duration).toBeLessThan(1000);
});
it("gets correct PDF filename when multiple ones are present", function () {

View File

@ -628,39 +628,43 @@ describe("document", function () {
const kid2BisRef = Ref.get(266, 0);
const parentRef = Ref.get(358, 0);
const allFields = Object.create(null);
const allFieldsObj = Object.create(null);
for (const name of ["parent", "kid1", "kid2", "kid11"]) {
const buttonWidgetDict = new Dict();
buttonWidgetDict.set("Type", Name.get("Annot"));
buttonWidgetDict.set("Subtype", Name.get("Widget"));
buttonWidgetDict.set("FT", Name.get("Btn"));
buttonWidgetDict.set("T", name);
allFields[name] = buttonWidgetDict;
allFieldsObj[name] = buttonWidgetDict;
}
allFields.kid1.set("Kids", [kid11Ref]);
allFields.parent.set("Kids", [kid1Ref, kid2Ref, kid2BisRef]);
allFieldsObj.kid1.set("Kids", [kid11Ref]);
allFieldsObj.parent.set("Kids", [kid1Ref, kid2Ref, kid2BisRef]);
const xref = new XRefMock([
{ ref: parentRef, data: allFields.parent },
{ ref: kid1Ref, data: allFields.kid1 },
{ ref: kid11Ref, data: allFields.kid11 },
{ ref: kid2Ref, data: allFields.kid2 },
{ ref: kid2BisRef, data: allFields.kid2 },
{ ref: parentRef, data: allFieldsObj.parent },
{ ref: kid1Ref, data: allFieldsObj.kid1 },
{ ref: kid11Ref, data: allFieldsObj.kid11 },
{ ref: kid2Ref, data: allFieldsObj.kid2 },
{ ref: kid2BisRef, data: allFieldsObj.kid2 },
]);
acroForm.set("Fields", [parentRef]);
pdfDocument = getDocument(acroForm, xref);
fields = (await pdfDocument.fieldObjects).allFields;
for (const [name, objs] of Object.entries(fields)) {
fields[name] = objs.map(obj => obj.id);
}
const { allFields, orphanFields } = await pdfDocument.fieldObjects;
expect(fields["parent.kid1"]).toEqual(["314R"]);
expect(fields["parent.kid1.kid11"]).toEqual(["159R"]);
expect(fields["parent.kid2"]).toEqual(["265R", "266R"]);
expect(fields.parent).toEqual(["358R"]);
const objIds = Array.from(allFields.entries(), ([name, objs]) => [
name,
objs.map(obj => obj.id),
]);
expect(objIds).toEqual([
["parent", ["358R"]],
["parent.kid1", ["314R"]],
["parent.kid1.kid11", ["159R"]],
["parent.kid2", ["265R", "266R"]],
]);
expect(orphanFields.size).toEqual(3);
});
it("should get field objects with a circular `Parent` chain", async function () {
@ -693,9 +697,14 @@ describe("document", function () {
acroForm.set("Fields", [widgetRef]);
const pdfDocument = getDocument(acroForm, xref);
const { allFields } = await pdfDocument.fieldObjects;
expect(Object.keys(allFields)).toEqual([""]);
expect(allFields[""].map(obj => obj.id)).toEqual(["1R"]);
const { allFields, orphanFields } = await pdfDocument.fieldObjects;
const objIds = Array.from(allFields.entries(), ([name, objs]) => [
name,
objs.map(obj => obj.id),
]);
expect(objIds).toEqual([["", ["1R"]]]);
expect(orphanFields.size).toEqual(0);
});
it("should check if fields have any actions", async function () {

View File

@ -0,0 +1,151 @@
/* Copyright 2026 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { FontFaceObject } from "../../src/display/font_loader.js";
import { isNodeJS } from "../../src/shared/util.js";
describe("font_loader", function () {
describe("FontFaceObject", function () {
function createFontFaceObject(fontFamily) {
return new FontFaceObject({
cssFontInfo: { fontFamily, fontWeight: "400", italicAngle: "0" },
data: new Uint8Array([0x00]),
disableFontFace: false,
fontExtraProperties: false,
loadedName: "g_d0_f1",
mimetype: "font/opentype",
});
}
function getFontFamily(rule) {
const start = rule.indexOf("font-family:") + "font-family:".length;
return rule.slice(start, rule.indexOf(";font-weight:", start));
}
it("creates a font-face rule", function () {
expect(
getFontFamily(createFontFaceObject("Foo-Bar").createFontFaceRule())
).toEqual(`"Foo-Bar"`);
});
it("keeps an injected rule inside the font family <string> (issue GHSA-wxrh-xgw3-3wqf)", function () {
const fontFamily = `"};body{background-image:url(https://example.com/)};a{x:"`;
const rule = createFontFaceObject(fontFamily).createFontFaceRule();
// The value already is a well-formed <string>, hence it's kept as-is.
expect(getFontFamily(rule)).toEqual(fontFamily);
});
it("serializes a font family which isn't a well-formed <string>", function () {
expect(
getFontFamily(
createFontFaceObject(String.raw`Foo"Bar\Baz`).createFontFaceRule()
)
).toEqual(String.raw`"Foo\"Bar\\Baz"`);
// A trailing backslash would otherwise escape the closing quote.
expect(
getFontFamily(
createFontFaceObject(
String.raw`"};body{background-image:url(https://example.com/)};a{x:\"`
).createFontFaceRule()
)
).toEqual(
String.raw`"\"};body{background-image:url(https://example.com/)};a{x:\\\""`
);
// A trailing backslash would otherwise escape the following semi-colon,
// thus swallowing the `font-weight` declaration.
expect(
getFontFamily(createFontFaceObject("Foo\\").createFontFaceRule())
).toEqual(String.raw`"Foo\\"`);
});
it("escapes CSS line terminators", function () {
const rule = createFontFaceObject(
"safe\f}body{background-image:url(https://example.com/)}/*"
).createFontFaceRule();
expect(rule).not.toContain("\f");
expect(getFontFamily(rule)).toEqual(
String.raw`"safe\c }body{background-image:url(https://example.com/)}/*"`
);
});
it("quotes generic families and CSS-wide keywords", function () {
// Those are not valid font family names, hence the `font-family`
// descriptor would be ignored if they were emitted unquoted.
for (const fontFamily of ["serif", "monospace", "inherit", "initial"]) {
expect(
getFontFamily(createFontFaceObject(fontFamily).createFontFaceRule())
).toEqual(`"${fontFamily}"`);
}
});
it("uses the same font family in both font loading paths", function () {
const NativeFontFace = globalThis.FontFace;
globalThis.FontFace = function MockFontFace(family) {
this.family = family;
};
try {
for (const fontFamily of [`"Foo Bar"`, "Foo-Bar", "serif"]) {
const font = createFontFaceObject(fontFamily);
expect(font.createNativeFontFace().family).toEqual(
getFontFamily(font.createFontFaceRule())
);
}
} finally {
globalThis.FontFace = NativeFontFace;
}
});
it("cannot escape the @font-face rule", function () {
if (isNodeJS) {
pending("Document is not supported in Node.js.");
}
const style = document.createElement("style");
document.head.append(style);
try {
for (const fontFamily of [
`"};body{background-image:url(https://example.com/)};a{x:"`,
String.raw`"};body{background-image:url(https://example.com/)};a{x:\"`,
"safe\f}body{background-image:url(https://example.com/)}/*",
String.raw`Foo"Bar\Baz`,
"Foo\\",
"serif",
]) {
const rule = createFontFaceObject(fontFamily).createFontFaceRule();
style.sheet.insertRule(rule, style.sheet.cssRules.length);
const cssRule = [...style.sheet.cssRules].at(-1);
expect(cssRule.constructor.name)
.withContext(fontFamily)
.toEqual("CSSFontFaceRule");
// The `font-family` descriptor must be both present and complete,
// i.e. the value must not have been truncated nor dropped.
expect(cssRule.style.getPropertyValue("font-family"))
.withContext(fontFamily)
.not.toEqual("");
}
// No additional rules were injected.
expect(style.sheet.cssRules.length).toEqual(6);
} finally {
style.remove();
}
});
});
});

View File

@ -72,6 +72,7 @@ async function initializePDFJS(callback) {
"pdfjs-test/unit/evaluator_spec.js",
"pdfjs-test/unit/event_utils_spec.js",
"pdfjs-test/unit/fetch_stream_spec.js",
"pdfjs-test/unit/font_loader_spec.js",
"pdfjs-test/unit/font_substitutions_spec.js",
"pdfjs-test/unit/fonts_spec.js",
"pdfjs-test/unit/image_utils_spec.js",

View File

@ -17,6 +17,7 @@ import {
createHeaders,
createResponseError,
extractFilenameFromHeader,
trimHeadersEnd,
validateRangeRequestCapabilities,
} from "../../src/display/network_utils.js";
import { ResponseException } from "../../src/shared/util.js";
@ -386,4 +387,38 @@ describe("network_utils", function () {
testCreateResponseError(new URL("https://foo.com/bar.pdf"), 0, false);
});
});
describe("trimHeadersEnd", function () {
it("removes the trailing whitespace", function () {
expect(trimHeadersEnd("a: 1\r\nb: 2\r\n")).toEqual("a: 1\r\nb: 2");
expect(trimHeadersEnd("a: 1\n\n")).toEqual("a: 1");
expect(trimHeadersEnd("a: 1\t\r\n")).toEqual("a: 1");
});
it("keeps the regular spaces", function () {
expect(trimHeadersEnd("a: 1 ")).toEqual("a: 1 ");
expect(trimHeadersEnd("a: 1\r\n ")).toEqual("a: 1\r\n ");
expect(trimHeadersEnd(" ")).toEqual(" ");
});
it("handles strings without trailing whitespace", function () {
expect(trimHeadersEnd("")).toEqual("");
expect(trimHeadersEnd("a: 1")).toEqual("a: 1");
expect(trimHeadersEnd("\r\na: 1")).toEqual("\r\na: 1");
});
it("handles a long run of whitespace efficiently", function () {
// Removing the run with a `$`-anchored regex is quadratic in its length,
// and a server controls how long the headers are.
const run = "\t".repeat(100000);
const startTime = performance.now();
// The run is trailing, hence removed.
expect(trimHeadersEnd(`a: 1${run}`)).toEqual("a: 1");
// The run is followed by a non-whitespace, hence kept: this is the case
// which a regex has to backtrack over.
expect(trimHeadersEnd(`a: 1${run}b`)).toEqual(`a: 1${run}b`);
expect(performance.now() - startTime).toBeLessThan(1000);
});
});
});

View File

@ -363,6 +363,80 @@ describe("Scripting", function () {
});
});
it("should trigger an event added with setAction", async () => {
const refId = getId();
const data = {
objects: {
field: [
{
id: refId,
value: "",
actions: {},
type: "text",
},
],
},
appInfo: { language: "en-US", platform: "Linux x86_64" },
calculationOrder: [],
};
sandbox.createSandbox(data);
await myeval(
`(this.getField("field").setAction("test", 'event.source.value = "abc";'), 0)`
);
await sandbox.dispatchEventInSandbox({
id: refId,
value: "",
name: "test",
willCommit: true,
});
expect(send_queue.has(refId)).toBeTrue();
expect(send_queue.get(refId)).toEqual({
id: refId,
value: "abc",
});
});
it("should trigger an event appended with setAction", async () => {
const refId = getId();
const data = {
objects: {
field: [
{
id: refId,
value: "",
actions: {
test: [`event.source.value = "a";`],
},
type: "text",
},
],
},
appInfo: { language: "en-US", platform: "Linux x86_64" },
calculationOrder: [],
};
sandbox.createSandbox(data);
await myeval(
`(this.getField("field").setAction("test", 'event.source.value += "b";'), 0)`
);
await sandbox.dispatchEventInSandbox({
id: refId,
value: "",
name: "test",
willCommit: true,
});
expect(send_queue.has(refId)).toBeTrue();
expect(send_queue.get(refId)).toEqual({
id: refId,
value: "ab",
});
});
it("should trigger a Keystroke event and invalidate it", async () => {
const refId = getId();
const data = {

View File

@ -38,10 +38,9 @@ const WASM_URL = isNodeJS
class DefaultFileReaderFactory {
static async fetch(params) {
if (isNodeJS) {
return fetchDataNode(params.path);
}
return fetchDataDOM(params.path, /* type = */ "bytes");
return isNodeJS
? fetchDataNode(params.path)
: fetchDataDOM(params.path, /* type = */ "bytes");
}
}

View File

@ -295,10 +295,20 @@ describe("Writer", function () {
});
it("should not use scientific notation for very large numbers", async function () {
// JavaScript produces scientific notation above ~1e21 but such values
// are unlikely in PDFs; values below that threshold must be plain.
// JavaScript's toString() and toFixed() produce scientific notation from
// 1e21 on, which is invalid PDF: such a number must be written with all
// its digits, which are the exact ones of the underlying double.
expect(await serialize(1e10)).toEqual("10000000000");
expect(await serialize(1.5e6)).toEqual("1500000");
expect(await serialize(1e20)).toEqual("100000000000000000000");
expect(await serialize(1e21)).toEqual("1000000000000000000000");
// Removing the trailing zeros of the exponent used to change the value:
// "1e+30" was written "1e+3" and "1e+100" was written "1e+1".
expect(await serialize(1e30)).toEqual("1000000000000000019884624838656");
expect(await serialize(-1e30)).toEqual(
"-1000000000000000019884624838656"
);
expect((await serialize(1e100)).length).toEqual(101);
});
it("should round to at most 10 decimal places", async function () {

View File

@ -109,6 +109,27 @@ describe("XML", function () {
});
});
describe("character references", function () {
const parseText = xml =>
new SimpleXMLParser({}).parseFromString(xml).documentElement.textContent;
it("should resolve the valid ones", function () {
expect(parseText("<a>&#65;&#x42;&#x1F602;&#0;&#x10FFFF;</a>")).toEqual(
"AB\u{1F602}\0\u{10FFFF}"
);
});
it("should keep the invalid ones as-is", function () {
// These must not throw: `String.fromCodePoint` rejects anything which
// isn't a code point.
expect(parseText("<a>&#xZZ;</a>")).toEqual("&#xZZ;");
expect(parseText("<a>&#zz;</a>")).toEqual("&#zz;");
expect(parseText("<a>&#x110000;</a>")).toEqual("&#x110000;");
expect(parseText("<a>&#1114112;</a>")).toEqual("&#1114112;");
expect(parseText("<a>&#-1;</a>")).toEqual("&#-1;");
});
});
it("should parse processing instructions", function () {
const xml = `
<a>
@ -132,4 +153,33 @@ describe("XML", function () {
["foo", ""],
]);
});
describe("entities", function () {
const parseText = xml =>
new SimpleXMLParser({}).parseFromString(xml).documentElement.textContent;
it("should resolve the entities", function () {
expect(
parseText("<a>&lt;b&gt; &amp; &quot;c&quot; &apos;d&apos;</a>")
).toEqual(`<b> & "c" 'd'`);
expect(parseText("<a>&#65;&#x42;</a>")).toEqual("AB");
});
it("should keep the unresolved entities as-is", function () {
expect(parseText("<a>&unknown; a&b;c</a>")).toEqual("&unknown; a&b;c");
});
it("should resolve an entity preceded by a bare ampersand", function () {
expect(parseText("<a>AT&T &amp; Co</a>")).toEqual("AT&T & Co");
expect(parseText("<a>&&amp;</a>")).toEqual("&&");
});
it("should handle a long run of ampersands efficiently", function () {
const text = "&".repeat(100000);
const startTime = performance.now();
expect(parseText(`<a>${text}</a>`)).toEqual(text);
expect(performance.now() - startTime).toBeLessThan(1000);
});
});
});

View File

@ -21,15 +21,18 @@
:root {
--editor-toolbar-vert-offset: 6px;
--outline-width: 2px;
--outline-color: #0060df;
/* Palette primitives, not the semantic light-dark tokens: these are drawn on
the always-light PDF page, where --color-accent-primary and friends would
resolve to their dark value. */
--outline-color: var(--color-blue-60, #0060df);
--outline-around-width: 1px;
--outline-around-color: #f0f0f4;
--outline-around-color: var(--color-gray-20, #f0f0f4);
--hover-outline-around-color: var(--outline-around-color);
--focus-outline: solid var(--outline-width) var(--outline-color);
--editor-selection-outline: solid var(--outline-width) var(--outline-color);
--unfocus-outline: solid var(--outline-width) transparent;
--focus-outline-around: solid var(--outline-around-width)
var(--outline-around-color);
--hover-outline-color: #8f8f9d;
--hover-outline-color: var(--color-gray-60, #8f8f9d);
--hover-outline: solid var(--outline-width) var(--hover-outline-color);
--hover-outline-around: solid var(--outline-around-width)
var(--hover-outline-around-color);
@ -95,7 +98,7 @@
--outline-color: CanvasText;
--outline-around-color: ButtonFace;
--resizer-bg-color: ButtonText;
--hover-outline-color: Highlight;
--hover-outline-color: var(--color-accent-primary-hover, Highlight);
--hover-outline-around-color: SelectedItemText;
}
}
@ -188,7 +191,7 @@
}
&.selectedEditor {
border: var(--focus-outline);
border: var(--editor-selection-outline);
outline: var(--focus-outline-around);
&::before {
@ -244,30 +247,51 @@
--editor-toolbar-bg-color: light-dark(#f0f0f4, #2b2a33);
--editor-toolbar-highlight-image: url(images/toolbarButton-editorHighlight.svg);
--editor-toolbar-fg-color: light-dark(#2e2e56, #fbfbfe);
--editor-toolbar-border-color: #8f8f9d;
--editor-toolbar-border-color: var(--border-color-interactive, #8f8f9d);
--editor-toolbar-hover-border-color: var(--editor-toolbar-border-color);
--editor-toolbar-hover-bg-color: light-dark(#e0e0e6, #52525e);
--editor-toolbar-hover-fg-color: var(--editor-toolbar-fg-color);
--editor-toolbar-hover-outline: none;
--editor-toolbar-focus-outline-color: light-dark(#0060df, #0df);
--editor-toolbar-focus-outline-color: var(
--focus-outline-color,
light-dark(#0060df, #0df)
);
--editor-toolbar-shadow: 0 2px 6px 0 rgb(58 57 68 / 0.2);
--editor-toolbar-height: 28px;
--editor-toolbar-padding: 2px;
--alt-text-done-color: light-dark(#2ac3a2, #54ffbd);
--alt-text-done-color: var(
--color-accent-attention,
light-dark(#2ac3a2, #54ffbd)
);
--alt-text-warning-color: light-dark(#0090ed, #80ebff);
--alt-text-hover-done-color: var(--alt-text-done-color);
--alt-text-hover-warning-color: var(--alt-text-warning-color);
@media screen and (forced-colors: active) {
--editor-toolbar-bg-color: ButtonFace;
--editor-toolbar-fg-color: ButtonText;
--editor-toolbar-border-color: ButtonText;
--editor-toolbar-hover-border-color: AccentColor;
--editor-toolbar-hover-bg-color: ButtonFace;
--editor-toolbar-hover-fg-color: AccentColor;
--editor-toolbar-bg-color: var(--button-background-color, ButtonFace);
--editor-toolbar-fg-color: var(--button-text-color, ButtonText);
--editor-toolbar-border-color: var(--button-border-color, ButtonText);
--editor-toolbar-hover-border-color: var(
--button-border-color-hover,
AccentColor
);
--editor-toolbar-hover-bg-color: var(
--button-background-color-hover,
ButtonFace
);
--editor-toolbar-hover-fg-color: var(
--button-text-color-hover,
AccentColor
);
--editor-toolbar-hover-outline: 2px solid
var(--editor-toolbar-hover-border-color);
--editor-toolbar-focus-outline-color: ButtonBorder;
/* --button-border-color, not --focus-outline-color: the ring is painted
on the toolbar's own ButtonFace surface, and --focus-outline-color is
CanvasText in Firefox HCM. */
--editor-toolbar-focus-outline-color: var(
--button-border-color,
ButtonBorder
);
--editor-toolbar-shadow: none;
--alt-text-done-color: var(--editor-toolbar-fg-color);
--alt-text-warning-color: var(--editor-toolbar-fg-color);
@ -461,15 +485,18 @@
&.show {
--alt-text-tooltip-bg: light-dark(#f0f0f4, #1c1b22);
--alt-text-tooltip-fg: light-dark(#15141a, #fbfbfe);
--alt-text-tooltip-border: #8f8f9d;
--alt-text-tooltip-fg: var(
--text-color,
light-dark(#15141a, #fbfbfe)
);
--alt-text-tooltip-border: var(--border-color-interactive, #8f8f9d);
--alt-text-tooltip-shadow: 0 2px 6px 0
light-dark(rgb(58 57 68 / 0.2), #15141a);
@media screen and (forced-colors: active) {
--alt-text-tooltip-bg: Canvas;
--alt-text-tooltip-fg: CanvasText;
--alt-text-tooltip-border: CanvasText;
--alt-text-tooltip-fg: var(--text-color, CanvasText);
--alt-text-tooltip-border: var(--border-color, CanvasText);
--alt-text-tooltip-shadow: none;
}
@ -1055,7 +1082,10 @@
}
.colorPicker {
--hover-outline-color: light-dark(#0250bb, #80ebff);
--hover-outline-color: var(
--color-accent-primary-hover,
light-dark(#0250bb, #80ebff)
);
--selected-outline-color: light-dark(#0060df, #aaf2ff);
--swatch-border-color: light-dark(#cfcfd8, #52525e);

View File

@ -370,6 +370,7 @@ const PDFViewerApplication = {
// Set some specific preferences for tests.
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
Object.assign(opts, {
annotationEditorMode: x => parseInt(x, 10),
capCanvasAreaFactor: x => parseInt(x, 10),
docBaseUrl: x => x,
enableAltText: x => x === "true",

View File

@ -416,6 +416,18 @@ const defaultOptions = new Map([
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
],
[
// Whether the viewer follows the Firefox design system (see the pref-gated
// @import of tokens-brand.css in viewer.css). Read from CSS via
// -moz-pref(), not from JS, so it is Firefox-only and has no effect
// elsewhere.
"enableNova",
{
/** @type {boolean} */
value: true,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
],
[
"enableOptimizedPartialRendering",
{

View File

@ -136,10 +136,13 @@ class Autolinker {
static #numericTLDRegex;
static findLinks(text) {
// Regex can be tested and verified at https://regex101.com/r/rXoLiT/2.
// Regex can be tested and verified at https://regex101.com/r/riHjvK/1.
// The email parts are bounded to keep the scan linear: a local part can't
// exceed 64 characters (RFC 5321, section 4.5.3.1.1) and a domain label
// can't exceed 63 (RFC 1035, section 2.3.4).
this.#regex ??=
// 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;
/\b(?:https?:\/\/|mailto:|www\.)(?:[\S--[\p{P}<>]]|\/|[\S--[\[\]]]+[\S--[\p{P}<>]])+|(?=\p{L})[\S--[@\p{Ps}\p{Pe}<>]]{1,64}@([\S--[[\p{P}--\-]<>]]{1,63}(?:\.[\S--[[\p{P}--\-]<>]]{1,63})+)/gv;
const [normalizedText, diffs] = normalize(text, { ignoreDashEOL: true });
const matches = normalizedText.matchAll(this.#regex);

247
web/buttons.css Normal file
View File

@ -0,0 +1,247 @@
/* Copyright 2026 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Shared button primitive: .primaryButton / .secondaryButton, for any button in
* the viewer and not only those inside a .dialog. The colors consume Firefox
* design tokens with the shipped literal as a fallback (see pdf_viewer.css).
*/
:root {
--button-primary-bg-color: var(
--button-background-color-primary,
light-dark(#0060df, #0df)
);
--button-primary-fg-color: var(
--button-text-color-primary,
light-dark(#fbfbfe, #15141a)
);
--button-primary-border-color: var(--button-primary-bg-color);
--button-primary-active-bg-color: var(
--button-background-color-primary-active,
light-dark(#054096, #aaf2ff)
);
--button-primary-active-fg-color: var(--button-primary-fg-color);
--button-primary-active-border-color: var(--button-primary-active-bg-color);
--button-primary-hover-bg-color: var(
--button-background-color-primary-hover,
light-dark(#0250bb, #80ebff)
);
--button-primary-hover-fg-color: var(--button-primary-fg-color);
--button-primary-hover-border-color: var(--button-primary-hover-bg-color);
--button-primary-disabled-bg-color: var(--button-primary-bg-color);
--button-primary-disabled-border-color: var(--button-primary-border-color);
--button-primary-disabled-fg-color: var(--button-primary-fg-color);
--button-secondary-bg-color: var(
--button-background-color,
light-dark(rgb(21 20 26 / 0.07), rgb(251 251 254 / 0.07))
);
--button-secondary-fg-color: var(--text-color, light-dark(#15141a, #fbfbfe));
/* Firefox's outline-style secondary button has a transparent fill and carries
the edge on --button-border-color; the fallback matches the fill instead,
since the shipped one is a translucent overlay. */
--button-secondary-border-color: var(
--button-border-color,
var(--button-secondary-bg-color)
);
--button-secondary-active-bg-color: var(
--button-background-color-active,
light-dark(rgb(21 20 26 / 0.21), rgb(251 251 254 / 0.21))
);
--button-secondary-active-fg-color: var(--button-secondary-fg-color);
--button-secondary-active-border-color: var(--button-secondary-bg-color);
--button-secondary-hover-bg-color: var(
--button-background-color-hover,
light-dark(rgb(21 20 26 / 0.14), rgb(251 251 254 / 0.14))
);
--button-secondary-hover-fg-color: var(--button-secondary-fg-color);
--button-secondary-hover-border-color: var(--button-secondary-hover-bg-color);
--button-secondary-disabled-bg-color: var(--button-secondary-bg-color);
--button-secondary-disabled-border-color: var(
--button-secondary-border-color
);
--button-secondary-disabled-fg-color: var(--button-secondary-fg-color);
--button-disabled-opacity: 0.4;
--hover-filter: brightness(0.9);
@media (prefers-color-scheme: dark) {
--button-disabled-opacity: 0.6;
--hover-filter: brightness(1.4);
}
@media screen and (forced-colors: active) {
--button-primary-bg-color: var(
--button-background-color-primary,
ButtonText
);
--button-primary-fg-color: var(--button-text-color-primary, ButtonFace);
--button-primary-border-color: var(
--button-border-color-primary,
ButtonText
);
--button-primary-active-bg-color: var(
--button-background-color-primary-active,
SelectedItem
);
--button-primary-active-fg-color: var(
--button-text-color-primary-active,
HighlightText
);
--button-primary-active-border-color: var(
--button-border-color-primary-active,
ButtonText
);
--button-primary-hover-bg-color: var(
--button-background-color-primary-hover,
SelectedItem
);
--button-primary-hover-fg-color: var(
--button-text-color-primary-hover,
HighlightText
);
--button-primary-hover-border-color: var(
--button-border-color-primary-hover,
SelectedItem
);
--button-primary-disabled-bg-color: var(
--button-background-color-primary-disabled,
GrayText
);
--button-primary-disabled-fg-color: var(
--button-text-color-primary-disabled,
ButtonFace
);
--button-primary-disabled-border-color: var(
--button-border-color-primary-disabled,
GrayText
);
--button-secondary-bg-color: var(--button-background-color, ButtonFace);
--button-secondary-fg-color: var(--button-text-color, ButtonText);
--button-secondary-border-color: var(--button-border-color, ButtonText);
--button-secondary-active-bg-color: var(
--button-background-color-active,
HighlightText
);
--button-secondary-active-fg-color: var(
--button-text-color-active,
SelectedItem
);
--button-secondary-active-border-color: var(
--button-border-color-active,
ButtonText
);
--button-secondary-hover-bg-color: var(
--button-background-color-hover,
HighlightText
);
--button-secondary-hover-fg-color: var(
--button-text-color-hover,
SelectedItem
);
--button-secondary-hover-border-color: var(
--button-border-color-hover,
SelectedItem
);
--button-secondary-disabled-fg-color: var(
--button-text-color-disabled,
GrayText
);
--button-secondary-disabled-border-color: var(
--button-border-color-disabled,
GrayText
);
--button-disabled-opacity: 1;
--hover-filter: none;
}
}
.primaryButton,
.secondaryButton {
border-radius: var(--button-border-radius, 4px);
border: 1px solid;
font: menu;
font-weight: var(--button-font-weight, 590);
font-size: var(--button-font-size, 13px);
padding: var(--button-padding, 4px 16px);
width: auto;
min-height: var(--button-min-height, 32px);
&:hover {
cursor: pointer;
filter: var(--hover-filter);
}
> span {
color: inherit;
font: inherit;
}
&:disabled {
pointer-events: none;
}
}
.primaryButton {
color: var(--button-primary-fg-color);
background-color: var(--button-primary-bg-color);
border-color: var(--button-primary-border-color);
opacity: 1;
&:hover {
color: var(--button-primary-hover-fg-color);
background-color: var(--button-primary-hover-bg-color);
border-color: var(--button-primary-hover-border-color);
}
&:active {
color: var(--button-primary-active-fg-color);
background-color: var(--button-primary-active-bg-color);
border-color: var(--button-primary-active-border-color);
}
&:disabled {
background-color: var(--button-primary-disabled-bg-color);
border-color: var(--button-primary-disabled-border-color);
color: var(--button-primary-disabled-fg-color);
opacity: var(--button-disabled-opacity);
}
}
.secondaryButton {
color: var(--button-secondary-fg-color);
background-color: var(--button-secondary-bg-color);
border-color: var(--button-secondary-border-color);
&:hover {
color: var(--button-secondary-hover-fg-color);
background-color: var(--button-secondary-hover-bg-color);
border-color: var(--button-secondary-hover-border-color);
}
&:active {
color: var(--button-secondary-active-fg-color);
background-color: var(--button-secondary-active-bg-color);
border-color: var(--button-secondary-active-border-color);
}
&:disabled {
background-color: var(--button-secondary-disabled-bg-color);
border-color: var(--button-secondary-disabled-border-color);
color: var(--button-secondary-disabled-fg-color);
opacity: var(--button-disabled-opacity);
}
}

View File

@ -65,39 +65,86 @@
:is(.annotationLayer, .annotationEditorLayer) {
.annotationCommentButton {
color-scheme: light dark;
--comment-button-bg: light-dark(white, #1c1b22);
--comment-button-bg: var(
--background-color-canvas,
light-dark(white, #1c1b22)
);
--comment-button-fg: light-dark(#5b5b66, #fbfbfe);
--comment-button-active-bg: light-dark(#0041a4, #a6ecf4);
--comment-button-active-fg: light-dark(white, #15141a);
--comment-button-hover-bg: light-dark(#0053cb, #61dce9);
--comment-button-hover-fg: light-dark(white, #15141a);
--comment-button-selected-bg: light-dark(#0062fa, #00cadb);
--comment-button-border-color: light-dark(#8f8f9d, #bfbfc9);
--comment-button-active-fg: var(
--button-text-color-primary,
light-dark(white, #15141a)
);
--comment-button-hover-bg: var(
--color-accent-primary-hover,
light-dark(#0053cb, #61dce9)
);
--comment-button-hover-fg: var(
--button-text-color-primary,
light-dark(white, #15141a)
);
--comment-button-selected-bg: var(
--color-accent-primary,
light-dark(#0062fa, #00cadb)
);
--comment-button-border-color: var(
--border-color-interactive,
light-dark(#8f8f9d, #bfbfc9)
);
--comment-button-active-border-color: var(--comment-button-active-bg);
--comment-button-focus-border-color: light-dark(#cfcfd8, #3a3944);
--comment-button-hover-border-color: var(--comment-button-hover-bg);
--comment-button-selected-border-color: var(--comment-button-selected-bg);
--comment-button-selected-fg: light-dark(white, #15141a);
--comment-button-selected-fg: var(
--button-text-color-primary,
light-dark(white, #15141a)
);
--comment-button-dim: 24px;
--comment-button-box-shadow:
0 0.25px 0.75px 0 light-dark(rgb(0 0 0 / 0.05), rgb(0 0 0 / 0.2)),
0 2px 6px 0 light-dark(rgb(0 0 0 / 0.1), rgb(0 0 0 / 0.4));
--comment-button-focus-outline-color: light-dark(#0062fa, #00cadb);
--comment-button-focus-outline-color: var(
--focus-outline-color,
light-dark(#0062fa, #00cadb)
);
/* Each state reads the *matching* pair of Firefox button tokens, so the
background and the icon can never resolve to the same system colour. */
@media screen and (forced-colors: active) {
--comment-button-bg: ButtonFace;
--comment-button-fg: ButtonText;
--comment-button-hover-bg: SelectedItemText;
--comment-button-hover-fg: SelectedItem;
--comment-button-active-bg: SelectedItemText;
--comment-button-active-fg: SelectedItem;
--comment-button-border-color: ButtonBorder;
--comment-button-active-border-color: ButtonBorder;
--comment-button-hover-border-color: SelectedItem;
--comment-button-bg: var(--button-background-color, ButtonFace);
--comment-button-fg: var(--button-text-color, ButtonText);
--comment-button-hover-bg: var(
--button-background-color-hover,
SelectedItemText
);
--comment-button-hover-fg: var(--button-text-color-hover, SelectedItem);
--comment-button-active-bg: var(
--button-background-color-active,
SelectedItemText
);
--comment-button-active-fg: var(--button-text-color-active, SelectedItem);
--comment-button-border-color: var(--button-border-color, ButtonBorder);
--comment-button-active-border-color: var(
--button-border-color-active,
ButtonBorder
);
--comment-button-hover-border-color: var(
--button-border-color-hover,
SelectedItem
);
--comment-button-box-shadow: none;
--comment-button-focus-outline-color: CanvasText;
--comment-button-selected-bg: ButtonBorder;
--comment-button-selected-fg: ButtonFace;
--comment-button-focus-outline-color: var(
--focus-outline-color,
CanvasText
);
--comment-button-selected-bg: var(
--button-background-color-primary,
ButtonBorder
);
--comment-button-selected-fg: var(
--button-text-color-primary,
ButtonFace
);
}
position: absolute;
@ -185,19 +232,28 @@
--comment-active-brightness: 0.825;
--comment-active-filter: brightness(var(--comment-active-brightness));
--comment-border-color: light-dark(#f0f0f4, #52525e);
--comment-focus-outline-color: light-dark(#0062fa, #00cadb);
--comment-fg-color: light-dark(#15141a, #fbfbfe);
--comment-focus-outline-color: var(
--focus-outline-color,
light-dark(#0062fa, #00cadb)
);
--comment-fg-color: var(--text-color, light-dark(#15141a, #fbfbfe));
--comment-count-bg-color: light-dark(#e2f7ff, #00317e);
--comment-indicator-active-fg-color: light-dark(#0041a4, #a6ecf4);
--comment-indicator-active-filter: brightness(
calc(1 / var(--comment-active-brightness))
);
--comment-indicator-focus-fg-color: light-dark(#5b5b66, #fbfbfe);
--comment-indicator-hover-fg-color: light-dark(#0053cb, #61dce9);
--comment-indicator-hover-fg-color: var(
--color-accent-primary-hover,
light-dark(#0053cb, #61dce9)
);
--comment-indicator-hover-filter: brightness(
calc(1 / var(--comment-hover-brightness))
);
--comment-indicator-selected-fg-color: light-dark(#0062fa, #00cadb);
--comment-indicator-selected-fg-color: var(
--color-accent-primary,
light-dark(#0062fa, #00cadb)
);
--button-comment-bg: transparent;
--button-comment-color: var(--main-color);
@ -208,9 +264,6 @@
--button-comment-hover-bg: light-dark(#e0e0e6, #52525e);
--button-comment-hover-color: var(--button-comment-color);
--link-fg-color: light-dark(#0060df, #0df);
--link-hover-fg-color: light-dark(#0250bb, #80ebff);
@media screen and (forced-colors: active) {
--comment-date-fg-color: CanvasText;
--comment-bg-color: Canvas;
@ -219,11 +272,13 @@
--comment-active-bg-color: Canvas;
--comment-active-filter: none;
--comment-border-color: CanvasText;
--comment-fg-color: CanvasText;
--comment-fg-color: var(--text-color, CanvasText);
--comment-count-bg-color: Canvas;
--comment-indicator-active-fg-color: SelectedItem;
--comment-indicator-focus-fg-color: CanvasText;
--comment-indicator-hover-fg-color: CanvasText;
--comment-indicator-focus-fg-color: var(--text-color, CanvasText);
/* On the Canvas comment surface, so paired with --text-color rather than a
button/accent token. */
--comment-indicator-hover-fg-color: var(--text-color, CanvasText);
--comment-indicator-selected-fg-color: SelectedItem;
--button-comment-bg: ButtonFace;
--button-comment-color: ButtonText;
@ -232,8 +287,6 @@
--button-comment-border: 1px solid ButtonText;
--button-comment-hover-bg: Highlight;
--button-comment-hover-color: HighlightText;
--link-fg-color: LinkText;
--link-hover-fg-color: LinkText;
}
}

View File

@ -14,117 +14,42 @@
*/
.dialog {
--dialog-bg-color: light-dark(white, #1c1b22);
--dialog-border-color: light-dark(white, #1c1b22);
--dialog-bg-color: var(--background-color-canvas, light-dark(white, #1c1b22));
--dialog-border-color: var(
--background-color-canvas,
light-dark(white, #1c1b22)
);
--dialog-shadow: 0 2px 14px 0 light-dark(rgb(58 57 68 / 0.2), #15141a);
--text-primary-color: light-dark(#15141a, #fbfbfe);
--text-secondary-color: light-dark(#5b5b66, #cfcfd8);
--hover-filter: brightness(0.9);
--link-fg-color: light-dark(#0060df, #0df);
--link-hover-fg-color: light-dark(#0250bb, #80ebff);
--separator-color: light-dark(#f0f0f4, #52525e);
--textarea-border-color: #8f8f9d;
/* A textarea is an interactive control, so it takes the interactive border
token (whose light value is the shipped literal) and not --border-color,
which is the low-contrast decorative one. */
--textarea-border-color: var(--border-color-interactive, #8f8f9d);
--textarea-bg-color: light-dark(white, #42414d);
--textarea-fg-color: var(--text-secondary-color);
--radio-bg-color: light-dark(#f0f0f4, #2b2a33);
--radio-checked-bg-color: light-dark(#fbfbfe, #15141a);
--radio-border-color: #8f8f9d;
--radio-checked-border-color: light-dark(#0060df, #0df);
--button-secondary-bg-color: light-dark(
rgb(21 20 26 / 0.07),
rgb(251 251 254 / 0.07)
);
--button-secondary-fg-color: var(--text-primary-color);
--button-secondary-border-color: var(--button-secondary-bg-color);
--button-secondary-active-bg-color: light-dark(
rgb(21 20 26 / 0.21),
rgb(251 251 254 / 0.21)
);
--button-secondary-active-fg-color: var(--button-secondary-fg-color);
--button-secondary-active-border-color: var(--button-secondary-bg-color);
--button-secondary-hover-bg-color: light-dark(
rgb(21 20 26 / 0.14),
rgb(251 251 254 / 0.14)
);
--button-secondary-hover-fg-color: var(--button-secondary-fg-color);
--button-secondary-hover-border-color: var(--button-secondary-hover-bg-color);
--button-secondary-disabled-bg-color: var(--button-secondary-bg-color);
--button-secondary-disabled-border-color: var(
--button-secondary-border-color
);
--button-secondary-disabled-fg-color: var(--button-secondary-fg-color);
--button-primary-bg-color: light-dark(#0060df, #0df);
--button-primary-fg-color: light-dark(#fbfbfe, #15141a);
--button-primary-border-color: var(--button-primary-bg-color);
--button-primary-active-bg-color: light-dark(#054096, #aaf2ff);
--button-primary-active-fg-color: var(--button-primary-fg-color);
--button-primary-active-border-color: var(--button-primary-active-bg-color);
--button-primary-hover-bg-color: light-dark(#0250bb, #80ebff);
--button-primary-hover-fg-color: var(--button-primary-fg-color);
--button-primary-hover-border-color: var(--button-primary-hover-bg-color);
--button-primary-disabled-bg-color: var(--button-primary-bg-color);
--button-primary-disabled-border-color: var(--button-primary-border-color);
--button-primary-disabled-fg-color: var(--button-primary-fg-color);
--button-disabled-opacity: 0.4;
/* .primaryButton / .secondaryButton button tokens are defined once, at :root
in buttons.css (the shared primitive), and inherit into dialogs. */
--input-text-bg-color: light-dark(white, #42414d);
--input-text-fg-color: var(--text-primary-color);
@media (prefers-color-scheme: dark) {
--hover-filter: brightness(1.4);
--button-disabled-opacity: 0.6;
}
@media screen and (forced-colors: active) {
--dialog-bg-color: Canvas;
--dialog-border-color: CanvasText;
--dialog-bg-color: var(--background-color-canvas, Canvas);
/* border-only: --background-color-canvas would resolve to Canvas in FF HCM
(= the dialog bg) and the shadow is none here, leaving no boundary; route
through --border-color (CanvasText in HCM) to keep a visible border. */
--dialog-border-color: var(--border-color, CanvasText);
--dialog-shadow: none;
--text-primary-color: CanvasText;
--text-secondary-color: CanvasText;
--hover-filter: none;
--link-fg-color: LinkText;
--link-hover-fg-color: LinkText;
--separator-color: CanvasText;
--textarea-border-color: ButtonBorder;
--textarea-bg-color: Field;
--textarea-fg-color: ButtonText;
--radio-bg-color: ButtonFace;
--radio-checked-bg-color: ButtonFace;
--radio-border-color: ButtonText;
--radio-checked-border-color: ButtonText;
--button-secondary-bg-color: ButtonFace;
--button-secondary-fg-color: ButtonText;
--button-secondary-border-color: ButtonText;
--button-secondary-active-bg-color: HighlightText;
--button-secondary-active-fg-color: SelectedItem;
--button-secondary-active-border-color: ButtonText;
--button-secondary-hover-bg-color: HighlightText;
--button-secondary-hover-fg-color: SelectedItem;
--button-secondary-hover-border-color: SelectedItem;
--button-secondary-disabled-fg-color: GrayText;
--button-secondary-disabled-border-color: GrayText;
--button-primary-bg-color: ButtonText;
--button-primary-fg-color: ButtonFace;
--button-primary-border-color: ButtonText;
--button-primary-active-bg-color: SelectedItem;
--button-primary-active-fg-color: HighlightText;
--button-primary-active-border-color: ButtonText;
--button-primary-hover-bg-color: SelectedItem;
--button-primary-hover-fg-color: HighlightText;
--button-primary-hover-border-color: SelectedItem;
--button-primary-disabled-bg-color: GrayText;
--button-primary-disabled-fg-color: ButtonFace;
--button-primary-disabled-border-color: GrayText;
--button-disabled-opacity: 1;
--input-text-bg-color: Field;
--input-text-fg-color: FieldText;
}
@ -133,7 +58,7 @@
font-size: 13px;
font-weight: 400;
line-height: 150%;
border-radius: 4px;
border-radius: var(--border-radius-medium, 4px);
padding: 12px 16px;
border: 1px solid var(--dialog-border-color);
background: var(--dialog-bg-color);
@ -176,6 +101,15 @@
align-self: flex-end;
}
/* Native checkbox/radio tinted with the accent at Firefox's control size,
matching Firefox's default form controls (the custom appearance is
Nova-only in Firefox, so the default look is native + accent-color). */
input:is([type="checkbox"], [type="radio"]) {
accent-color: var(--color-accent-primary, light-dark(#0060df, #0df));
width: var(--checkbox-size, 16px);
height: var(--checkbox-size, 16px);
}
.radio {
display: flex;
flex-direction: column;
@ -187,25 +121,6 @@
gap: 8px;
align-self: stretch;
align-items: center;
input {
appearance: none;
box-sizing: border-box;
width: 16px;
height: 16px;
border-radius: 50%;
background-color: var(--radio-bg-color);
border: 1px solid var(--radio-border-color);
&:hover {
filter: var(--hover-filter);
}
&:checked {
background-color: var(--radio-checked-bg-color);
border: 4px solid var(--radio-checked-border-color);
}
}
}
> .radioLabel {
@ -224,14 +139,15 @@
}
button:not(:is(.toggle-button, .closeButton, .clearInputButton)) {
border-radius: 4px;
border: 1px solid;
border-radius: var(--button-border-radius, 4px);
border-width: 1px;
border-style: solid;
font: menu;
font-weight: 590;
font-size: 13px;
padding: 4px 16px;
font-weight: var(--button-font-weight, 590);
font-size: var(--button-font-size, 13px);
padding: var(--button-padding, 4px 16px);
width: auto;
height: 32px;
min-height: var(--button-min-height, 32px);
&:hover {
cursor: pointer;
@ -243,57 +159,9 @@
font: inherit;
}
&.secondaryButton {
color: var(--button-secondary-fg-color);
background-color: var(--button-secondary-bg-color);
border-color: var(--button-secondary-border-color);
&:hover {
color: var(--button-secondary-hover-fg-color);
background-color: var(--button-secondary-hover-bg-color);
border-color: var(--button-secondary-hover-border-color);
}
&:active {
color: var(--button-secondary-active-fg-color);
background-color: var(--button-secondary-active-bg-color);
border-color: var(--button-secondary-active-border-color);
}
&:disabled {
background-color: var(--button-secondary-disabled-bg-color);
border-color: var(--button-secondary-disabled-border-color);
color: var(--button-secondary-disabled-fg-color);
opacity: var(--button-disabled-opacity);
}
}
&.primaryButton {
color: var(--button-primary-fg-color);
background-color: var(--button-primary-bg-color);
border-color: var(--button-primary-border-color);
opacity: 1;
&:hover {
color: var(--button-primary-hover-fg-color);
background-color: var(--button-primary-hover-bg-color);
border-color: var(--button-primary-hover-border-color);
}
&:active {
color: var(--button-primary-active-fg-color);
background-color: var(--button-primary-active-bg-color);
border-color: var(--button-primary-active-border-color);
}
&:disabled {
background-color: var(--button-primary-disabled-bg-color);
border-color: var(--button-primary-disabled-border-color);
color: var(--button-primary-disabled-fg-color);
opacity: var(--button-disabled-opacity);
}
}
/* .primaryButton / .secondaryButton colours and states come from the
shared buttons.css primitive; only the dialog-specific base geometry
(above) lives here, so it also covers unclassed dialog buttons. */
&:disabled {
pointer-events: none;
}
@ -313,7 +181,7 @@
resize: none;
margin: 0;
box-sizing: border-box;
border-radius: 4px;
border-radius: var(--border-radius-small, 4px);
border: 1px solid var(--textarea-border-color);
background: var(--textarea-bg-color);
color: var(--textarea-fg-color);

View File

@ -28,16 +28,28 @@
--sig-detail-color: light-dark(rgb(96 96 96), rgb(180 180 184));
--sig-divider-color: light-dark(rgb(228 228 232), rgb(82 82 86));
--sig-summary-hover-color: light-dark(rgb(28 67 138), rgb(126 169 255));
--sig-link-color: light-dark(rgb(28 113 216), rgb(126 169 255));
--sig-link-color: var(
--link-color,
light-dark(rgb(28 113 216), rgb(126 169 255))
);
--sig-link-hover-bg: light-dark(
rgb(28 113 216 / 0.1),
rgb(126 169 255 / 0.15)
);
--sig-banner-verified-bg: light-dark(rgb(228 247 235), rgb(28 84 49));
--sig-banner-verified-bg: var(
--background-color-success,
light-dark(rgb(228 247 235), rgb(28 84 49))
);
--sig-banner-verified-color: light-dark(rgb(16 92 47), rgb(176 232 196));
--sig-banner-warn-bg: light-dark(rgb(255 247 217), rgb(95 67 9));
--sig-banner-warn-bg: var(
--background-color-warning,
light-dark(rgb(255 247 217), rgb(95 67 9))
);
--sig-banner-warn-color: light-dark(rgb(124 84 9), rgb(255 222 153));
--sig-banner-error-bg: light-dark(rgb(254 226 235), rgb(122 21 51));
--sig-banner-error-bg: var(
--background-color-critical,
light-dark(rgb(254 226 235), rgb(122 21 51))
);
--sig-banner-error-color: light-dark(rgb(167 26 70), rgb(255 188 207));
/* Tint colours for the row / toolbar icons. These are paired with
@ -48,9 +60,18 @@
* verified = green (only used for the top-level "everything fine"
* row and the toolbar's verified badge). */
--sig-icon-default: light-dark(rgb(150 150 150), rgb(180 180 184));
--sig-icon-warn: light-dark(rgb(217 142 27), rgb(255 178 77));
--sig-icon-error: light-dark(rgb(196 31 71), rgb(255 117 145));
--sig-icon-verified: light-dark(rgb(29 142 61), rgb(106 210 126));
--sig-icon-warn: var(
--icon-color-warning,
light-dark(rgb(217 142 27), rgb(255 178 77))
);
--sig-icon-error: var(
--icon-color-critical,
light-dark(rgb(196 31 71), rgb(255 117 145))
);
--sig-icon-verified: var(
--icon-color-success,
light-dark(rgb(29 142 61), rgb(106 210 126))
);
@media screen and (forced-colors: active) {
/* HCM keywords are picked by *semantic role*, not by hue the
@ -77,23 +98,26 @@
--sig-detail-color: GrayText;
--sig-divider-color: GrayText;
--sig-summary-hover-color: AccentColor;
--sig-link-color: LinkText;
--sig-link-color: var(--link-color, LinkText);
--sig-link-hover-bg: transparent;
--sig-banner-verified-bg: ButtonFace;
--sig-banner-verified-color: ButtonText;
--sig-banner-warn-bg: ButtonFace;
--sig-banner-warn-color: ButtonText;
--sig-banner-error-bg: ButtonFace;
--sig-banner-error-color: ButtonText;
/* Firefox collapses the status backgrounds onto --background-color-canvas
in HCM, so the foregrounds have to follow --text-color to stay a pair
(ButtonText over that Canvas surface is not guaranteed to contrast). */
--sig-banner-verified-bg: var(--background-color-success, ButtonFace);
--sig-banner-verified-color: var(--text-color, ButtonText);
--sig-banner-warn-bg: var(--background-color-warning, ButtonFace);
--sig-banner-warn-color: var(--text-color, ButtonText);
--sig-banner-error-bg: var(--background-color-critical, ButtonFace);
--sig-banner-error-color: var(--text-color, ButtonText);
/* Severities collapse to the same emphasis keyword (AccentColor)
* in HCM same trick as alt-text where `done` and `warning`
* share their hover colour. The glyph shape (check vs `!` vs ``)
* carries the remaining distinction. The neutral "row crypto
* verified" check stays muted (GrayText). */
--sig-icon-default: GrayText;
--sig-icon-warn: AccentColor;
--sig-icon-error: AccentColor;
--sig-icon-verified: AccentColor;
--sig-icon-warn: var(--icon-color-warning, AccentColor);
--sig-icon-error: var(--icon-color-critical, AccentColor);
--sig-icon-verified: var(--icon-color-success, AccentColor);
}
}

View File

@ -14,6 +14,7 @@
*/
import { INTERNAL_EVT, internalOpt } from "./internal_evt.js";
import { makeSet } from "pdfjs-lib";
const WaitOnType = {
EVENT: "event",
@ -72,7 +73,7 @@ async function waitOnEventOrTimeout({ target, name, delay = 0 }) {
* and `off` methods. To raise an event, the `dispatch` method shall be used.
*/
class EventBus {
#listeners = Object.create(null);
#listeners = new Map();
constructor() {
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
@ -101,8 +102,7 @@ class EventBus {
signal.addEventListener("abort", onAbort);
}
const eventListeners = (this.#listeners[eventName] ??= []);
eventListeners.push({
this.#listeners.getOrInsertComputed(eventName, makeSet).add({
listener,
internal: options?.internal === INTERNAL_EVT,
once: options?.once === true,
@ -116,17 +116,11 @@ class EventBus {
* @param {Object} [options]
*/
off(eventName, listener, options = null) {
const eventListeners = this.#listeners[eventName];
if (!eventListeners) {
return;
}
for (let i = 0, ii = eventListeners.length; i < ii; i++) {
const evt = eventListeners[i];
if (evt.listener === listener) {
const eventListeners = this.#listeners.get(eventName);
const evt = eventListeners?.keys().find(e => e.listener === listener);
if (evt) {
evt.rmAbort?.(); // Ensure that the `AbortSignal` listener is removed.
eventListeners.splice(i, 1);
return;
}
eventListeners.delete(evt);
}
}
@ -135,14 +129,14 @@ class EventBus {
* @param {Object} data
*/
dispatch(eventName, data) {
const eventListeners = this.#listeners[eventName];
if (!eventListeners?.length) {
const eventListeners = this.#listeners.get(eventName);
if (!eventListeners?.size) {
return;
}
let extListeners;
// Making copy of the listeners array in case if it will be modified
// Always create a copy of the listeners in case they are modified
// during dispatch.
for (const { listener, internal, once } of eventListeners.slice(0)) {
for (const { listener, internal, once } of new Set(eventListeners)) {
if (once) {
this.off(eventName, listener);
}

View File

@ -41,10 +41,13 @@ button.hasPopupMenu {
var(--menu-text-color),
transparent 93%
);
--menuitem-focus-outline-color: light-dark(#0062fa, #00cadb);
--menuitem-focus-outline-color: var(
--focus-outline-color,
light-dark(#0062fa, #00cadb)
);
--menuitem-focus-border-color: light-dark(white, black);
--menu-bg: light-dark(white, #23222b);
--menu-bg: var(--background-color-box, light-dark(white, #23222b));
--menu-background-blend-mode: normal;
--menu-box-shadow:
0 0.375px 1.5px 0 light-dark(rgb(0 0 0 / 0.05), rgb(0 0 0 / 0.2)),
@ -52,7 +55,7 @@ button.hasPopupMenu {
--menu-border-color: light-dark(rgb(21 20 26 / 0.1), rgb(251 251 254 / 0.1));
--menuitem-border-radius: 8px;
--menu-backdrop-filter: none;
--menu-text-color: light-dark(#15141a, #fbfbfe);
--menu-text-color: var(--text-color, light-dark(#15141a, #fbfbfe));
--menu-text-disabled-color: var(--menu-text-color);
--menuitem-text-hover-fg: var(--menu-text-color);
--menuitem-hover-bg: color-mix(
@ -64,11 +67,11 @@ button.hasPopupMenu {
--disabled-opacity: 0.62;
@media screen and (forced-colors: active) {
--menu-bg: Canvas;
--menu-bg: var(--background-color-box, Canvas);
--menu-background-blend-mode: normal;
--menu-box-shadow: none;
--menu-backdrop-filter: none;
--menu-text-color: ButtonText;
--menu-text-color: var(--text-color, ButtonText);
--menu-text-disabled-color: GrayText;
--menu-border-color: CanvasText;
--menuitem-border-color: none;
@ -78,7 +81,7 @@ button.hasPopupMenu {
--menuitem-active-border-color: ButtonText;
--menuitem-text-active-fg: SelectedItem;
--menuitem-focus-bg: ButtonFace;
--menuitem-focus-outline-color: CanvasText;
--menuitem-focus-outline-color: var(--focus-outline-color, CanvasText);
--menuitem-focus-border-color: none;
--disabled-opacity: 1;
}

View File

@ -146,19 +146,11 @@ class Menu {
stopEvent(e);
break;
case "Home":
this.#menuItems
.find(
item => !item.disabled && !item.classList.contains("hidden")
)
?.focus();
this.#goToFirstLast(false);
stopEvent(e);
break;
case "End":
this.#menuItems
.findLast(
item => !item.disabled && !item.classList.contains("hidden")
)
?.focus();
this.#goToFirstLast(true);
stopEvent(e);
break;
default:
@ -194,11 +186,7 @@ class Menu {
if (!this.#openMenuAC) {
this.#openMenu();
}
this.#menuItems
.find(
item => !item.disabled && !item.classList.contains("hidden")
)
?.focus();
this.#goToFirstLast(false);
break;
case "ArrowUp":
case "End":
@ -206,11 +194,7 @@ class Menu {
if (!this.#openMenuAC) {
this.#openMenu();
}
this.#menuItems
.findLast(
item => !item.disabled && !item.classList.contains("hidden")
)
?.focus();
this.#goToFirstLast(true);
break;
case "Escape":
this.#closeMenu();
@ -252,6 +236,20 @@ class Menu {
}
}
/**
* Go to the first/last menu item.
* @param {boolean} [last]
*/
#goToFirstLast(last = false) {
const i = this.#menuItems[last ? "findLastIndex" : "findIndex"](
item => !item.disabled && !item.classList.contains("hidden")
);
if (i >= 0) {
this.#menuItems[i].focus();
this.#lastIndex = i;
}
}
destroy() {
this.#closeMenu();
this.#menuAC?.abort();

View File

@ -17,28 +17,46 @@
--closing-button-icon: url(images/messageBar_closingButton.svg);
--message-bar-close-button-color: var(--text-primary-color);
--message-bar-close-button-color-hover: var(--text-primary-color);
--message-bar-close-button-border-radius: 4px;
--message-bar-close-button-border-radius: var(--border-radius-small, 4px);
--message-bar-close-button-border: none;
--message-bar-close-button-hover-bg-color: light-dark(
rgb(21 20 26 / 0.14),
rgb(251 251 254 / 0.14)
--message-bar-close-button-hover-bg-color: var(
--button-background-color-hover,
light-dark(rgb(21 20 26 / 0.14), rgb(251 251 254 / 0.14))
);
--message-bar-close-button-active-bg-color: light-dark(
rgb(21 20 26 / 0.21),
rgb(251 251 254 / 0.21)
--message-bar-close-button-active-bg-color: var(
--button-background-color-active,
light-dark(rgb(21 20 26 / 0.21), rgb(251 251 254 / 0.21))
);
--message-bar-close-button-focus-bg-color: light-dark(
rgb(21 20 26 / 0.07),
rgb(251 251 254 / 0.07)
--message-bar-close-button-focus-bg-color: var(
--button-background-color,
light-dark(rgb(21 20 26 / 0.07), rgb(251 251 254 / 0.07))
);
@media screen and (forced-colors: active) {
--message-bar-close-button-color: ButtonText;
--message-bar-close-button-border: 1px solid ButtonText;
--message-bar-close-button-hover-bg-color: ButtonText;
--message-bar-close-button-active-bg-color: ButtonText;
--message-bar-close-button-focus-bg-color: ButtonText;
--message-bar-close-button-color-hover: HighlightText;
--message-bar-close-button-color: var(--button-text-color, ButtonText);
--message-bar-close-button-border: 1px solid
var(--button-border-color, ButtonText);
/* One icon colour is shared by hover/active/focus, so all three backgrounds
read the -hover token and pair with --button-text-color-hover. The
fallbacks are SelectedItemText/SelectedItem for the same reason: the
previous ButtonText-behind-HighlightText hid the icon outright in any
theme whose button ink and highlight text are both light. */
--message-bar-close-button-hover-bg-color: var(
--button-background-color-hover,
SelectedItemText
);
--message-bar-close-button-active-bg-color: var(
--button-background-color-hover,
SelectedItemText
);
--message-bar-close-button-focus-bg-color: var(
--button-background-color-hover,
SelectedItemText
);
--message-bar-close-button-color-hover: var(
--button-text-color-hover,
SelectedItem
);
}
display: flex;
@ -50,7 +68,9 @@
gap: 8px;
user-select: none;
border-radius: 4px;
/* Nests inside .dialog, whose radius is --border-radius-medium, so it has to
scale with it to keep the inner corner tighter than the outer one. */
border-radius: var(--border-radius-small, 4px);
border: 1px solid var(--message-bar-border-color);
background: var(--message-bar-bg-color);
@ -130,8 +150,6 @@
}
#editorUndoBar {
--text-primary-color: light-dark(#15141a, #fbfbfe);
--message-bar-icon: url(images/messageBar_info.svg);
--message-bar-icon-color: light-dark(#0060df, #73a7f3);
--message-bar-bg-color: light-dark(#deeafc, #003070);
@ -141,41 +159,10 @@
rgb(255 255 255 / 0.08)
);
--undo-button-bg-color: light-dark(
rgb(21 20 26 / 0.07),
rgb(255 255 255 / 0.08)
);
--undo-button-bg-color-hover: light-dark(
rgb(21 20 26 / 0.14),
rgb(255 255 255 / 0.14)
);
--undo-button-bg-color-active: light-dark(
rgb(21 20 26 / 0.21),
rgb(255 255 255 / 0.21)
);
--undo-button-border: 1px solid light-dark(#0060df, #0df);
--undo-button-fg-color: var(--message-bar-fg-color);
--undo-button-fg-color-hover: var(--undo-button-fg-color);
--undo-button-fg-color-active: var(--undo-button-fg-color);
@media screen and (forced-colors: active) {
--text-primary-color: CanvasText;
--message-bar-icon-color: CanvasText;
--message-bar-bg-color: Canvas;
--message-bar-border-color: CanvasText;
--undo-button-bg-color: ButtonText;
--undo-button-bg-color-hover: SelectedItem;
--undo-button-bg-color-active: SelectedItem;
--undo-button-fg-color: ButtonFace;
--undo-button-fg-color-hover: SelectedItemText;
--undo-button-fg-color-active: SelectedItemText;
--undo-button-border: none;
}
position: fixed;
@ -197,26 +184,10 @@
}
#editorUndoBarUndoButton {
border-radius: 4px;
font-weight: 590;
line-height: 19.5px;
color: var(--undo-button-fg-color);
border: var(--undo-button-border);
padding: 4px 16px;
margin-inline-start: 8px;
height: 32px;
background-color: var(--undo-button-bg-color);
&:hover {
background-color: var(--undo-button-bg-color-hover);
color: var(--undo-button-fg-color-hover);
}
&:active {
background-color: var(--undo-button-bg-color-active);
color: var(--undo-button-fg-color-active);
}
/* The generic secondary border is the same translucent wash as its fill,
which disappears on the tinted bar, so keep an explicit accent edge. */
border-color: var(--color-accent-primary, light-dark(#0060df, #0df));
}
> div {

View File

@ -936,9 +936,14 @@ class PDFPageView extends BasePDFPageView {
if (this.structTreeLayer && !this.textLayer) {
this.structTreeLayer = null;
}
// The annotation editor layer and the draw layer keep references on the
// text layer (the latter uses its div in order to render the selection),
// hence they must be recreated too.
if (
this.annotationEditorLayer &&
(!keepAnnotationEditorLayer || !this.annotationEditorLayer.div)
(!keepAnnotationEditorLayer ||
!this.annotationEditorLayer.div ||
!this.textLayer)
) {
if (this.drawLayer) {
this.drawLayer.cancel();
@ -947,6 +952,10 @@ class PDFPageView extends BasePDFPageView {
this.annotationEditorLayer.cancel();
this.annotationEditorLayer = null;
}
if (this.drawLayer && !this.textLayer) {
this.drawLayer.cancel();
this.drawLayer = null;
}
if (this.xfaLayer && (!keepXfaLayer || !this.xfaLayer.div)) {
this.xfaLayer.cancel();
this.xfaLayer = null;

View File

@ -111,7 +111,7 @@ class PDFScriptingManager {
// targeting an unknown id can be ignored.
if (objects) {
this.#objectIds = new Set();
for (const fields of Object.values(objects)) {
for (const fields of objects.values()) {
for (const { id } of fields) {
this.#objectIds.add(id);
}

View File

@ -12,6 +12,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Firefox design-system bridge.
*
* The viewer chrome is authored against Firefox design-token names with the
* shipped literal as the fallback: `var(--fx-token, <literal>)`. GENERIC and
* the components build resolve the fallback and stay self-contained; the
* MOZCENTRAL build imports tokens-brand.css when pdfjs.enableNova is set (see
* viewer.css), so those names resolve to live Firefox values and the theme
* never has to be re-synced by hand.
*
* Firefox declares its tokens inside `@layer tokens-foundation-brand`, so any
* unlayered declaration wins over them. To *consume* a Firefox value,
* reference it through var() (with a fallback) and do NOT redeclare the same
* name: that would shadow Firefox's for the whole subtree.
*/
@import url(buttons.css);
@import url(message_bar.css);
@import url(dialog.css);
@import url(text_layer_builder.css);
@ -34,8 +50,12 @@
--page-border: 9px solid transparent;
--spreadHorizontalWrapped-margin-LR: -3.5px;
--loading-icon-delay: 400ms;
--focus-ring-color: light-dark(#0060df, #0df);
--focus-ring-outline: 2px solid var(--focus-ring-color);
--focus-ring-color: var(--focus-outline-color, light-dark(#0060df, #0df));
--focus-ring-outline: var(--focus-outline-width, 2px) solid
var(--focus-ring-color);
--text-primary-color: var(--text-color, light-dark(#15141a, #fbfbfe));
--link-fg-color: var(--link-color, light-dark(#0060df, #0df));
--link-hover-fg-color: var(--link-color-hover, light-dark(#0250bb, #80ebff));
--new-badge-bg: light-dark(#070, #37b847);
--new-badge-color: light-dark(#fff, #15141a);
--new-badge-border-color: light-dark(#fbfbfe / 40%, #15141a / 40%);
@ -45,7 +65,10 @@
--page-margin: 8px auto -1px;
--page-border: 1px solid CanvasText;
--spreadHorizontalWrapped-margin-LR: 3.5px;
--focus-ring-color: CanvasText;
--focus-ring-color: var(--focus-outline-color, CanvasText);
--text-primary-color: var(--text-color, CanvasText);
--link-fg-color: var(--link-color, LinkText);
--link-hover-fg-color: var(--link-color-hover, LinkText);
--new-badge-bg: AccentColor;
--new-badge-color: ButtonFace;
--new-badge-border-color: CanvasText;

View File

@ -1600,13 +1600,10 @@ class PDFViewer {
}
get #pageWidthScaleFactor() {
if (
this._spreadMode !== SpreadMode.NONE &&
return this._spreadMode !== SpreadMode.NONE &&
this._scrollMode !== ScrollMode.HORIZONTAL
) {
return 2;
}
return 1;
? 2
: 1;
}
#setScale(value, options) {
@ -1890,19 +1887,20 @@ class PDFViewer {
container.scrollLeft - firstPage.x,
container.scrollTop - firstPage.y
);
const intLeft = Math.round(topLeft[0]);
const intTop = Math.round(topLeft[1]);
const [left, top] = topLeft;
let pdfOpenParams = `#page=${pageNumber}`;
if (!this.isInPresentationMode) {
pdfOpenParams += `&zoom=${normalizedScaleValue},${intLeft},${intTop}`;
pdfOpenParams +=
`&zoom=${normalizedScaleValue},` +
`${Math.round(left)},${Math.round(top)}`;
}
this._location = {
pageNumber,
scale: normalizedScaleValue,
top: intTop,
left: intLeft,
top,
left,
rotation: this._pagesRotation,
pdfOpenParams,
};

View File

@ -14,7 +14,7 @@
*/
.sidebar {
--sidebar-bg-color: light-dark(#fff, #23222b);
--sidebar-bg-color: var(--background-color-box, light-dark(#fff, #23222b));
--sidebar-border-color: light-dark(
rgb(21 20 26 / 0.1),
rgb(251 251 254 / 0.1)
@ -30,13 +30,18 @@
--sidebar-width: 239px;
--resizer-width: 4px;
--resizer-shift: calc(0px - var(--resizer-width) - 2px);
--resizer-hover-bg-color: light-dark(#0062fa, #00cadb);
--resizer-hover-bg-color: var(
--color-accent-primary,
light-dark(#0062fa, #00cadb)
);
@media screen and (forced-colors: active) {
--sidebar-bg-color: Canvas;
--sidebar-bg-color: var(--background-color-box, Canvas);
--sidebar-border-color: CanvasText;
--sidebar-box-shadow: none;
--resizer-hover-bg-color: CanvasText;
/* Painted on the Canvas sidebar, so --text-color rather than the accent
(which is ButtonText in Firefox HCM). */
--resizer-hover-bg-color: var(--text-color, CanvasText);
}
border-radius: var(--sidebar-border-radius);

View File

@ -15,8 +15,16 @@
:root {
--clear-signature-button-icon: url(images/editor-toolbar-delete.svg);
--signature-bg: light-dark(#f9f9fb, #2b2a33);
--signature-hover-bg: light-dark(#f0f0f4, var(--signature-bg));
/* The signature wells are boxes nested in the dialog, i.e. Firefox's
.info-box-container pattern. */
--signature-bg: var(
--background-color-box-info,
light-dark(#f9f9fb, #2b2a33)
);
--signature-hover-bg: var(
--button-background-color-hover,
light-dark(#f0f0f4, var(--signature-bg))
);
--button-signature-bg: transparent;
--button-signature-color: var(--main-color);
--button-signature-active-bg: light-dark(#cfcfd8, #5b5b66);
@ -41,13 +49,13 @@
.signatureDialog {
--primary-color: var(--text-primary-color);
--border-color: #8f8f9d;
--signature-border-color: var(--border-color-interactive, #8f8f9d);
--open-link-fg: var(--link-fg-color);
--open-link-hover-fg: var(--link-hover-fg-color);
@media screen and (forced-colors: active) {
--primary-color: ButtonText;
--border-color: ButtonText;
--signature-border-color: var(--border-color-interactive, ButtonText);
--open-link-fg: ButtonText;
--open-link-hover-fg: ButtonText;
}
@ -88,11 +96,11 @@
> input {
width: 100%;
height: 32px;
height: var(--size-item-large, 32px);
padding-inline: 8px calc(4px + var(--button-dimension));
box-sizing: border-box;
border-radius: 4px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-small, 4px);
border: 1px solid var(--signature-border-color);
}
.clearInputButton {
@ -113,11 +121,17 @@
#addSignatureDialog {
--secondary-color: var(--text-secondary-color);
--bg-hover: #e0e0e6;
--tab-top-line-active-color: #0060df;
--bg-hover: var(
--button-background-color-hover,
light-dark(#e0e0e6, #52525e)
);
--tab-top-line-active-color: var(
--color-accent-primary,
light-dark(#0060df, #0df)
);
--tab-top-line-active-hover-color: var(--tab-text-hover-color);
--tab-top-line-hover-color: #8f8f9d;
--tab-top-line-inactive-color: #cfcfd8;
--tab-top-line-hover-color: var(--border-color-interactive, #8f8f9d);
--tab-top-line-inactive-color: light-dark(#cfcfd8, #8f8f9d);
--tab-bottom-line-active-color: var(--tab-top-line-inactive-color);
--tab-bottom-line-hover-color: var(--tab-top-line-inactive-color);
--tab-bottom-line-inactive-color: var(--tab-top-line-inactive-color);
@ -126,7 +140,7 @@
--tab-bg-active-hover-color: var(--bg-hover);
--tab-bg-hover: var(--bg-hover);
--tab-panel-border: none;
--tab-panel-border-radius: 4px;
--tab-panel-border-radius: var(--border-radius-small, 4px);
--tab-text-color: var(--primary-color);
--tab-text-active-color: var(--tab-top-line-active-color);
--tab-text-active-hover-color: var(--tab-text-hover-color);
@ -134,24 +148,6 @@
--signature-placeholder-color: var(--secondary-color);
--signature-draw-placeholder-color: var(--primary-color);
--signature-color: var(--primary-color);
--clear-signature-button-border-width: 0;
--clear-signature-button-border-style: solid;
--clear-signature-button-border-color: transparent;
--clear-signature-button-border-disabled-color: transparent;
--clear-signature-button-color: var(--primary-color);
--clear-signature-button-hover-color: var(--clear-signature-button-color);
--clear-signature-button-active-color: var(--clear-signature-button-color);
--clear-signature-button-disabled-color: var(--clear-signature-button-color);
--clear-signature-button-focus-color: var(--clear-signature-button-color);
--clear-signature-button-bg: var(--dialog-bg-color);
--clear-signature-button-bg-hover: var(--bg-hover);
--clear-signature-button-bg-active: #cfcfd8;
--clear-signature-button-bg-focus: #f0f0f4;
--clear-signature-button-bg-disabled: color-mix(
in srgb,
#f0f0f4,
transparent 40%
);
--save-warning-color: var(--secondary-color);
--thickness-bg: var(--dialog-bg-color);
--thickness-label-color: var(--primary-color);
@ -159,30 +155,13 @@
--thickness-border: none;
--draw-cursor: url(images/cursor-editorInk.svg) 0 16, pointer;
@media (prefers-color-scheme: dark) {
/* TODO: Update the dialog colors for dark mode but in dialog.css */
--dialog-bg-color: #42414d;
--bg-hover: #52525e;
--primary-color: #fbfbfe;
--secondary-color: #cfcfd8;
--tab-top-line-active-color: #0df;
--tab-top-line-inactive-color: #8f8f9d;
--clear-signature-button-bg-active: #5b5b66;
--clear-signature-button-bg-focus: #2b2a33;
--clear-signature-button-bg-disabled: color-mix(
in srgb,
#2b2a33,
transparent 40%
);
}
@media screen and (forced-colors: active) {
--secondary-color: ButtonText;
--bg: HighlightText;
--bg-hover: var(--bg);
--tab-top-line-active-color: ButtonText;
--tab-top-line-active-color: var(--color-accent-primary, ButtonText);
--tab-top-line-active-hover-color: HighlightText;
--tab-top-line-hover-color: SelectedItem;
--tab-top-line-hover-color: var(--border-color-interactive, SelectedItem);
--tab-top-line-inactive-color: ButtonText;
--tab-bottom-line-active-color: var(--tab-top-line-active-color);
--tab-bottom-line-hover-color: var(--tab-top-line-hover-color);
@ -196,24 +175,10 @@
--tab-text-active-hover-color: HighlightText;
--tab-text-hover-color: SelectedItem;
--signature-color: ButtonText;
--clear-signature-button-border-width: 1px;
--clear-signature-button-border-style: solid;
--clear-signature-button-border-color: ButtonText;
--clear-signature-button-border-disabled-color: GrayText;
--clear-signature-button-color: ButtonText;
--clear-signature-button-hover-color: HighlightText;
--clear-signature-button-active-color: SelectedItem;
--clear-signature-button-focus-color: CanvasText;
--clear-signature-button-disabled-color: GrayText;
--clear-signature-button-bg: var(--bg);
--clear-signature-button-bg-hover: SelectedItem;
--clear-signature-button-bg-active: var(--bg);
--clear-signature-button-bg-focus: var(--bg);
--clear-signature-button-bg-disabled: var(--bg);
--thickness-bg: Canvas;
--thickness-label-color: CanvasText;
--thickness-slider-color: ButtonText;
--thickness-border: 1px solid var(--border-color);
--thickness-border: 1px solid var(--signature-border-color);
}
#addSignatureDialogLabel {
@ -533,14 +498,7 @@
#clearSignatureButton {
display: flex;
height: 32px;
padding: 4px 8px;
align-items: center;
background-color: var(--clear-signature-button-bg);
border-width: var(--clear-signature-button-border-width);
border-style: var(--clear-signature-button-border-style);
border-color: var(--clear-signature-button-border-color);
border-radius: 4px;
> span {
display: flex;
@ -549,8 +507,6 @@
gap: 4px;
flex-shrink: 0;
color: var(--clear-signature-button-color);
&::after {
content: "";
display: inline-block;
@ -558,57 +514,10 @@
height: 16px;
mask-image: var(--clear-signature-button-icon);
mask-size: cover;
background-color: var(--clear-signature-button-color);
background-color: currentColor;
flex-shrink: 0;
}
}
&:hover {
background-color: var(--clear-signature-button-bg-hover);
> span {
color: var(--clear-signature-button-hover-color);
&::after {
background-color: var(--clear-signature-button-hover-color);
}
}
}
&:active {
background-color: var(--clear-signature-button-bg-active);
> span {
color: var(--clear-signature-button-active-color);
&::after {
background-color: var(--clear-signature-button-active-color);
}
}
}
&:focus-visible {
background-color: var(--clear-signature-button-bg-focus);
> span {
color: var(--clear-signature-button-focus-color);
&::after {
background-color: var(--clear-signature-button-focus-color);
}
}
}
&:disabled {
background-color: var(--clear-signature-button-bg-disabled);
border-color: var(--clear-signature-button-border-disabled-color);
> span {
color: var(--clear-signature-button-disabled-color);
&::after {
background-color: var(
--clear-signature-button-disabled-color
);
}
}
}
}
}

View File

@ -2,44 +2,13 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
.toggle-button {
--button-background-color: color-mix(in srgb, currentColor 7%, transparent);
--button-background-color-hover: color-mix(
in srgb,
currentColor 14%,
transparent
);
--button-background-color-active: color-mix(
in srgb,
currentColor 21%,
transparent
);
--color-accent-primary: light-dark(#0060df, #0df);
--color-accent-primary-hover: light-dark(#0250bb, #80ebff);
--color-accent-primary-active: light-dark(#054096, #aaf2ff);
--border-radius-circle: 9999px;
--border-width: 1px;
--size-item-small: 16px;
--size-item-large: 32px;
--color-canvas: light-dark(white, #1c1b22);
--background-color-canvas: var(--color-canvas);
--border-color-interactive: light-dark(#8f8f9d, #f9f9fa);
--border-color-interactive-hover: var(--border-color-interactive);
--border-color-interactive-active: var(--border-color-interactive);
--focus-outline-offset: 2px;
@media (forced-colors: active) {
--color-accent-primary: ButtonText;
--color-accent-primary-hover: SelectedItem;
--color-accent-primary-active: SelectedItem;
--button-background-color: ButtonFace;
--border-color-interactive: ButtonText;
--border-color-interactive-hover: SelectedItem;
--border-color-interactive-active: ButtonText;
--color-canvas: ButtonText;
--background-color-canvas: Canvas;
}
}
/* The Firefox foundation tokens below are read with the shipped literal as the
fallback, so MOZCENTRAL tracks the design system (tokens-brand.css, linked
from viewer.html) while every other build keeps this self-contained
moz-toggle copy. Declaring those token names here instead would shadow
Firefox's (unlayered beats @layer), and a bare var() would both trip
stylelint's no-unknown-custom-properties and collapse the toggle to a
zero-size transparent element if the chrome sheet ever failed to load. */
/*
The original file is located at:
@ -55,23 +24,47 @@
*/
.toggle-button {
--toggle-background-color: var(--button-background-color);
--toggle-background-color-hover: var(--button-background-color-hover);
--toggle-background-color-active: var(--button-background-color-active);
--toggle-background-color-pressed: var(--color-accent-primary);
--toggle-background-color-pressed-hover: var(--color-accent-primary-hover);
--toggle-background-color-pressed-active: var(--color-accent-primary-active);
--toggle-border-color: var(--border-color-interactive);
--toggle-background-color: var(
--button-background-color,
color-mix(in srgb, currentColor 7%, transparent)
);
--toggle-background-color-hover: var(
--button-background-color-hover,
color-mix(in srgb, currentColor 14%, transparent)
);
--toggle-background-color-active: var(
--button-background-color-active,
color-mix(in srgb, currentColor 21%, transparent)
);
--toggle-background-color-pressed: var(
--color-accent-primary,
light-dark(#0060df, #0df)
);
--toggle-background-color-pressed-hover: var(
--color-accent-primary-hover,
light-dark(#0250bb, #80ebff)
);
--toggle-background-color-pressed-active: var(
--color-accent-primary-active,
light-dark(#054096, #aaf2ff)
);
--toggle-border-color: var(
--border-color-interactive,
light-dark(#8f8f9d, #f9f9fa)
);
--toggle-border-color-hover: var(--toggle-border-color);
--toggle-border-color-active: var(--toggle-border-color);
--toggle-border-radius: var(--border-radius-circle);
--toggle-border-width: var(--border-width);
--toggle-height: var(--size-item-small);
--toggle-width: var(--size-item-large);
--toggle-border-radius: var(--border-radius-circle, 9999px);
--toggle-border-width: var(--border-width, 1px);
--toggle-height: var(--size-item-small, 16px);
--toggle-width: var(--size-item-large, 32px);
--toggle-dot-background-color: var(--toggle-border-color);
--toggle-dot-background-color-hover: var(--toggle-dot-background-color);
--toggle-dot-background-color-active: var(--toggle-dot-background-color);
--toggle-dot-background-color-on-pressed: var(--background-color-canvas);
--toggle-dot-background-color-on-pressed: var(
--background-color-canvas,
light-dark(white, #1c1b22)
);
--toggle-dot-margin: 1px;
--toggle-dot-height: calc(
var(--toggle-height) - 2 * var(--toggle-dot-margin) - 2 *
@ -93,8 +86,8 @@
box-sizing: border-box;
&:focus-visible {
outline: var(--focus-outline);
outline-offset: var(--focus-outline-offset);
outline: var(--focus-ring-outline);
outline-offset: var(--focus-outline-offset, 2px);
}
&:enabled:hover {
@ -186,16 +179,42 @@
@media (forced-colors) {
.toggle-button {
--toggle-dot-background-color: var(--color-accent-primary);
--toggle-dot-background-color-hover: var(--color-accent-primary-hover);
--toggle-dot-background-color-active: var(--color-accent-primary-active);
--toggle-dot-background-color-on-pressed: var(--button-background-color);
--toggle-border-color-hover: var(--border-color-interactive-hover);
--toggle-border-color-active: var(--border-color-interactive-active);
--toggle-background-color: var(--button-background-color, ButtonFace);
--toggle-background-color-pressed: var(--color-accent-primary, ButtonText);
--toggle-background-color-pressed-hover: var(
--color-accent-primary-hover,
SelectedItem
);
--toggle-background-color-pressed-active: var(
--color-accent-primary-active,
SelectedItem
);
--toggle-border-color: var(--border-color-interactive, ButtonText);
--toggle-dot-background-color: var(--color-accent-primary, ButtonText);
--toggle-dot-background-color-hover: var(
--color-accent-primary-hover,
SelectedItem
);
--toggle-dot-background-color-active: var(
--color-accent-primary-active,
SelectedItem
);
--toggle-dot-background-color-on-pressed: var(
--button-background-color,
ButtonFace
);
--toggle-border-color-hover: var(
--border-color-interactive-hover,
SelectedItem
);
--toggle-border-color-active: var(
--border-color-interactive-active,
ButtonText
);
}
.toggle-button[aria-pressed="true"]:enabled::after {
border: 1px solid var(--button-background-color);
border: 1px solid var(--button-background-color, ButtonFace);
content: "";
position: absolute;
height: var(--toggle-height);

View File

@ -26,29 +26,22 @@ const DEFAULT_VIEW_HISTORY_CACHE_SIZE = 20;
*/
class ViewHistory {
constructor(fingerprint, cacheSize = DEFAULT_VIEW_HISTORY_CACHE_SIZE) {
this.fingerprint = fingerprint;
this.cacheSize = cacheSize;
this._initializedPromise = this._readFromStorage().then(databaseStr => {
const database = JSON.parse(databaseStr || "{}");
let index = -1;
if (!Array.isArray(database.files)) {
database.files = [];
} else {
while (database.files.length >= this.cacheSize) {
while (database.files.length >= cacheSize) {
database.files.shift();
}
for (let i = 0, ii = database.files.length; i < ii; i++) {
const branch = database.files[i];
if (branch.fingerprint === this.fingerprint) {
index = i;
break;
}
}
index = database.files.findIndex(
branch => branch.fingerprint === fingerprint
);
}
if (index === -1) {
index = database.files.push({ fingerprint: this.fingerprint }) - 1;
index = database.files.push({ fingerprint }) - 1;
}
this.file = database.files[index];
this.database = database;

View File

@ -24,12 +24,6 @@
--field-bg-color: light-dark(rgb(255 255 255), rgb(64 64 68));
--field-border-color: light-dark(rgb(187 187 188), rgb(115 115 115));
--doorhanger-bg-color: light-dark(rgb(255 255 255), rgb(74 74 79));
--dialog-button-border: none;
--dialog-button-bg-color: light-dark(rgb(12 12 13 / 0.1), rgb(92 92 97));
--dialog-button-hover-bg-color: light-dark(
rgb(12 12 13 / 0.3),
rgb(115 115 115)
);
--toolbar-bg-color: light-dark(#f9f9fb, #2b2a33);
--toolbar-divider-color: light-dark(#e0e0e6, #5b5b66);
@ -42,9 +36,6 @@
@media screen and (forced-colors: active) {
:root {
--dialog-button-border: 1px solid Highlight;
--dialog-button-hover-bg-color: Highlight;
--dialog-button-hover-color: ButtonFace;
--field-border-color: ButtonText;
--main-color: CanvasText;
}
@ -94,40 +85,6 @@ body {
inset-block-start: 0;
}
.dialogButton {
border: none;
background: none;
width: 28px;
height: 28px;
outline: none;
}
.dialogButton:is(:hover, :focus-visible) {
background-color: var(--dialog-button-hover-bg-color);
}
.dialogButton:is(:hover, :focus-visible) > span {
color: var(--dialog-button-hover-color);
}
.dialogButton[disabled] {
opacity: 0.5;
}
.dialogButton {
min-width: 16px;
margin: 2px 1px;
padding: 2px 6px 0;
border: none;
border-radius: 2px;
color: var(--main-color);
font-size: 12px;
line-height: 14px;
user-select: none;
cursor: default;
box-sizing: border-box;
}
.toolbarField {
padding: 4px 7px;
margin: 3px 0;
@ -204,19 +161,6 @@ body {
}
}
:is(.toolbarButton .dialogButton)[disabled] {
opacity: 0.5;
}
.dialogButton {
width: auto;
margin: 3px 4px 2px !important;
padding: 2px 11px;
color: var(--main-color);
background-color: var(--dialog-button-bg-color);
border: var(--dialog-button-border) !important;
}
dialog {
margin: auto;
padding: 15px;

View File

@ -124,10 +124,10 @@ See https://github.com/adobe-type-tools/cmap-resources
<input type="password" id="password" class="toolbarField" />
</div>
<div class="buttonRow">
<button id="passwordCancel" class="dialogButton" type="button">
<button id="passwordCancel" class="secondaryButton" type="button">
<span data-l10n-id="pdfjs-password-cancel-button"></span>
</button>
<button id="passwordSubmit" class="dialogButton" type="button">
<button id="passwordSubmit" class="primaryButton" type="button">
<span data-l10n-id="pdfjs-password-ok-button"></span>
</button>
</div>

View File

@ -13,6 +13,16 @@
* limitations under the License.
*/
/* Pref-gated, so the Firefox tokens are opt-in (and with them the Nova theme,
which tokens-brand.css gates on browser.nova.enabled): with the pref off
every var() falls back to its shipped literal. -moz-pref() works here
because chrome rules are enabled for a resource:// stylesheet, and it only
sees pdfjs.* prefs. */
/*#if MOZCENTRAL*/
@import url(chrome://global/skin/design-system/tokens-brand.css) -moz-pref(
"pdfjs.enableNova"
);
/*#endif*/
@import url(pdf_viewer.css);
@import url(digital_signature_properties.css);
@ -55,7 +65,10 @@
inset calc(-1px * var(--dir-factor)) 0 0 rgb(0 0 0 / 0.25),
0 1px 0 rgb(0 0 0 / 0.15), 0 0 1px rgb(0 0 0 / 0.1);
--toolbarSidebar-border-bottom: none;
--button-hover-color: color-mix(in srgb, currentColor 17%, transparent);
--button-hover-color: var(
--button-background-color-toolbar-hover,
color-mix(in srgb, currentColor 17%, transparent)
);
--toggled-btn-color: light-dark(rgb(0 0 0), rgb(255 255 255));
--toggled-btn-bg-color: rgb(0 0 0 / 0.3);
--toggled-hover-active-btn-color: rgb(0 0 0 / 0.4);
@ -70,12 +83,6 @@
--doorhanger-border-color: light-dark(rgb(12 12 13 / 0.2), rgb(39 39 43));
--doorhanger-hover-color: light-dark(rgb(12 12 13), rgb(249 249 250));
--doorhanger-separator-color: light-dark(rgb(222 222 222), rgb(92 92 97));
--dialog-button-border: none;
--dialog-button-bg-color: light-dark(rgb(12 12 13 / 0.1), rgb(92 92 97));
--dialog-button-hover-bg-color: light-dark(
rgb(12 12 13 / 0.3),
rgb(115 115 115)
);
--loading-icon: url(images/loading.svg);
--toolbarButton-editorComment-icon: url(images/comment-editButton.svg);
@ -136,6 +143,10 @@
@media screen and (forced-colors: active) {
:root {
/* Must stay a system color in HCM: --button-background-color-toolbar-hover
is NOT overridden in the dist's forced-colors layer (it stays a
color-mix(currentColor 17%, transparent)), which would make the hover
background a faint translucent overlay and hide the ButtonFace icon. */
--button-hover-color: Highlight;
--toolbar-icon-opacity: 1;
--toolbar-icon-bg-color: ButtonText;
@ -150,9 +161,6 @@
--doorhanger-hover-color: ButtonFace;
--doorhanger-border-color-whcm: 1px solid ButtonText;
--doorhanger-triangle-opacity-whcm: 0;
--dialog-button-border: 1px solid Highlight;
--dialog-button-hover-bg-color: Highlight;
--dialog-button-hover-color: ButtonFace;
--dropdown-btn-border: 1px solid ButtonText;
--field-border-color: ButtonText;
--main-color: CanvasText;
@ -413,22 +421,6 @@ body {
}
}
.dialogButton {
border: none;
background: none;
width: 28px;
height: 28px;
outline: none;
}
.dialogButton:is(:hover, :focus-visible) {
background-color: var(--dialog-button-hover-bg-color);
}
.dialogButton:is(:hover, :focus-visible) > span {
color: var(--dialog-button-hover-color);
}
.splitToolbarButtonSeparator {
float: inline-start;
width: 0;
@ -437,20 +429,6 @@ body {
border-right: none;
}
.dialogButton {
min-width: 16px;
margin: 2px 1px;
padding: 2px 6px 0;
border: none;
border-radius: 2px;
color: var(--main-color);
font-size: 12px;
line-height: 14px;
user-select: none;
cursor: default;
box-sizing: border-box;
}
#viewsManagerToggleButton::before {
mask-image: var(--toolbarButton-viewsManagerToggle-icon);
transform: scaleX(var(--dir-factor));
@ -646,15 +624,6 @@ body {
}
}
.dialogButton {
width: auto;
margin: 3px 4px 2px !important;
padding: 2px 11px;
color: var(--main-color);
background-color: var(--dialog-button-bg-color);
border: var(--dialog-button-border) !important;
}
dialog {
margin: auto;
padding: 15px;
@ -665,7 +634,7 @@ dialog {
line-height: 14px;
background-color: var(--doorhanger-bg-color);
border: 1px solid rgb(0 0 0 / 0.5);
border-radius: 4px;
border-radius: var(--border-radius-medium, 4px);
box-shadow: 0 1px 4px rgb(0 0 0 / 0.3);
}
@ -695,8 +664,10 @@ dialog .separator {
}
dialog .buttonRow {
text-align: center;
vertical-align: middle;
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
dialog :link {

View File

@ -33,7 +33,7 @@ See https://github.com/adobe-type-tools/cmap-resources
<!--<link rel="icon" type="image/svg+xml" href="chrome://global/skin/icons/pdf.svg" />-->
<!--<meta
http-equiv="Content-Security-Policy"
content="default-src 'none'; script-src resource: 'wasm-unsafe-eval'; worker-src resource:; style-src resource:; img-src resource: blob: data:; media-src blob:; font-src resource:; connect-src resource:; base-uri 'none'; form-action 'none';"
content="default-src 'none'; script-src resource: 'wasm-unsafe-eval'; worker-src resource:; style-src resource: chrome:; img-src resource: blob: data:; media-src blob:; font-src resource:; connect-src resource:; base-uri 'none'; form-action 'none';"
/>-->
<!--#elif TESTING-->
<!--<meta
@ -901,8 +901,8 @@ See https://github.com/adobe-type-tools/cmap-resources
<input type="password" id="password" class="toolbarField" />
</div>
<div class="buttonRow">
<button id="passwordCancel" class="dialogButton" type="button"><span data-l10n-id="pdfjs-password-cancel-button"></span></button>
<button id="passwordSubmit" class="dialogButton" type="button"><span data-l10n-id="pdfjs-password-ok-button"></span></button>
<button id="passwordCancel" class="secondaryButton" type="button"><span data-l10n-id="pdfjs-password-cancel-button"></span></button>
<button id="passwordSubmit" class="primaryButton" type="button"><span data-l10n-id="pdfjs-password-ok-button"></span></button>
</div>
</dialog>
<dialog id="documentPropertiesDialog">
@ -966,7 +966,9 @@ See https://github.com/adobe-type-tools/cmap-resources
<p id="linearizedField" aria-labelledby="linearizedLabel">-</p>
</div>
<div class="buttonRow">
<button id="documentPropertiesClose" class="dialogButton" type="button"><span data-l10n-id="pdfjs-document-properties-close-button"></span></button>
<button id="documentPropertiesClose" class="secondaryButton" type="button">
<span data-l10n-id="pdfjs-document-properties-close-button"></span>
</button>
</div>
</dialog>
<dialog class="dialog altText" id="altTextDialog" aria-labelledby="dialogLabel" aria-describedby="dialogDescription">
@ -1210,7 +1212,7 @@ See https://github.com/adobe-type-tools/cmap-resources
<button class="clearInputButton" type="button" tabindex="0" aria-hidden="true"></button>
</span>
</div>
<button id="clearSignatureButton" type="button" data-l10n-id="pdfjs-editor-add-signature-clear-button" tabindex="0">
<button id="clearSignatureButton" class="secondaryButton" type="button" data-l10n-id="pdfjs-editor-add-signature-clear-button" tabindex="0">
<span data-l10n-id="pdfjs-editor-add-signature-clear-button-label"></span>
</button>
</div>
@ -1297,7 +1299,7 @@ See https://github.com/adobe-type-tools/cmap-resources
<span data-l10n-id="pdfjs-print-progress-percent" data-l10n-args='{ "progress": 0 }' class="relative-progress">0%</span>
</div>
<div class="buttonRow">
<button id="printCancel" class="dialogButton" type="button"><span data-l10n-id="pdfjs-print-progress-close-button"></span></button>
<button id="printCancel" class="secondaryButton" type="button"><span data-l10n-id="pdfjs-print-progress-close-button"></span></button>
</div>
</dialog>
<!--#endif-->
@ -1312,7 +1314,7 @@ See https://github.com/adobe-type-tools/cmap-resources
<div>
<span id="editorUndoBarMessage" class="description"></span>
</div>
<button id="editorUndoBarUndoButton" class="undoButton" type="button" tabindex="0" data-l10n-id="pdfjs-editor-undo-bar-undo-button">
<button id="editorUndoBarUndoButton" class="secondaryButton" type="button" tabindex="0" data-l10n-id="pdfjs-editor-undo-bar-undo-button">
<span data-l10n-id="pdfjs-editor-undo-bar-undo-button-label"></span>
</button>
<button id="editorUndoBarCloseButton" class="closeButton" type="button" tabindex="0" data-l10n-id="pdfjs-editor-undo-bar-close-button">

View File

@ -60,19 +60,35 @@
--sidebar-max-width: 50vw;
--sidebar-block-padding: 8px;
--text-color: light-dark(#15141a, #fbfbfe);
--button-fg: var(--text-color);
--button-no-bg: transparent;
--button-bg: light-dark(rgb(21 20 26 / 0.07), rgb(251 251 254 / 0.07));
--button-border-color: transparent;
--button-hover-bg: light-dark(rgb(21 20 26 / 0.14), rgb(251 251 254 / 0.14));
--button-hover-fg: var(--text-color);
--button-hover-border-color: var(--button-border-color);
--button-active-bg: light-dark(rgb(21 20 26 / 0.21), rgb(251 251 254 / 0.21));
--button-active-fg: var(--text-color);
--button-active-border-color: var(--button-border-color);
--button-focus-no-bg: color-mix(in srgb, var(--text-color), transparent 93%);
--button-focus-outline-color: light-dark(#0062fa, #00cadb);
--views-text-color: var(--text-color, light-dark(#15141a, #fbfbfe));
--button-fg: var(--views-text-color);
--button-no-bg: var(--button-background-color-toolbar, transparent);
--button-bg: var(
--button-background-color,
light-dark(rgb(21 20 26 / 0.07), rgb(251 251 254 / 0.07))
);
--views-button-border-color: var(--button-border-color, transparent);
--button-hover-bg: var(
--button-background-color-hover,
light-dark(rgb(21 20 26 / 0.14), rgb(251 251 254 / 0.14))
);
--button-hover-fg: var(--views-text-color);
--button-hover-border-color: var(--views-button-border-color);
--button-active-bg: var(
--button-background-color-active,
light-dark(rgb(21 20 26 / 0.21), rgb(251 251 254 / 0.21))
);
--button-active-fg: var(--views-text-color);
--button-active-border-color: var(--views-button-border-color);
--button-focus-no-bg: color-mix(
in srgb,
var(--views-text-color),
transparent 93%
);
--button-focus-outline-color: var(
--focus-outline-color,
light-dark(#0062fa, #00cadb)
);
--button-focus-border-color: light-dark(white, black);
--status-border-color: transparent;
--status-actions-bg: light-dark(
@ -81,7 +97,7 @@
);
--status-undo-bg: light-dark(rgb(0 98 250 / 0.08), rgb(0 202 219 / 0.08));
--status-waiting-bg: var(--status-undo-bg);
--indicator-color: light-dark(#0062fa, #00cadb);
--indicator-color: var(--color-accent-primary, light-dark(#0062fa, #00cadb));
--status-warning-bg: light-dark(#ffe8ea, #6e001f);
--indicator-warning-color: light-dark(#b20037, #ffa0aa);
--header-shadow:
@ -95,7 +111,7 @@
--image-current-border-color: var(--button-focus-outline-color);
--image-current-focused-outline-color: var(--image-hover-border-color);
--image-page-number-bg: light-dark(#f0f0f4, #42414d);
--image-page-number-fg: var(--text-color);
--image-page-number-fg: var(--views-text-color);
--image-page-number-border-color: transparent;
--image-hover-page-number-bg: var(--image-page-number-bg);
--image-hover-page-number-fg: var(--image-page-number-fg);
@ -134,23 +150,25 @@
--multiple-dragging-text-color: light-dark(#fbfbfe, #15141a);
@media screen and (forced-colors: active) {
--text-color: CanvasText;
--button-fg: ButtonText;
--button-bg: ButtonFace;
--button-no-bg: ButtonFace;
--button-border-color: ButtonText;
--button-hover-bg: SelectedItemText;
--button-hover-fg: SelectedItem;
--button-hover-border-color: SelectedItem;
--button-active-bg: SelectedItemText;
--button-active-fg: SelectedItem;
--button-active-border-color: ButtonText;
--button-focus-no-bg: ButtonFace;
--button-focus-outline-color: CanvasText;
--views-text-color: var(--text-color, CanvasText);
--button-fg: var(--button-text-color, ButtonText);
--button-bg: var(--button-background-color, ButtonFace);
--button-no-bg: var(--button-background-color, ButtonFace);
--views-button-border-color: var(--button-border-color, ButtonText);
--button-hover-bg: var(--button-background-color-hover, SelectedItemText);
--button-hover-fg: var(--button-text-color-hover, SelectedItem);
--button-hover-border-color: var(--button-border-color-hover, SelectedItem);
--button-active-bg: var(--button-background-color-active, SelectedItemText);
--button-active-fg: var(--button-text-color-active, SelectedItem);
--button-active-border-color: var(--button-border-color-active, ButtonText);
--button-focus-no-bg: var(--button-background-color, ButtonFace);
--button-focus-outline-color: var(--focus-outline-color, CanvasText);
--button-focus-border-color: none;
--status-border-color: CanvasText;
--status-undo-bg: none;
--indicator-color: CanvasText;
/* Painted on Canvas panels, so --text-color rather than the accent (which
is ButtonText in Firefox HCM). */
--indicator-color: var(--text-color, CanvasText);
--status-warning-bg: none;
--indicator-warning-color: CanvasText;
--header-shadow: none;
@ -162,7 +180,7 @@
--image-hover-page-number-fg: SelectedItem;
--image-current-page-number-bg: ButtonText;
--image-current-page-number-fg: ButtonFace;
--image-current-border-color: ButtonText;
--image-current-border-color: var(--border-color, ButtonText);
--image-current-focused-outline-color: var(--image-hover-border-color);
--image-current-hover-page-number-bg: SelectedItem;
--image-current-hover-page-number-fg: SelectedItemText;
@ -170,7 +188,7 @@
--image-page-number-fg: ButtonText;
--image-page-number-border-color: var(--image-page-number-fg);
--multiple-dragging-bg: Canvas;
--multiple-dragging-indicator-bg: ButtonBorder;
--multiple-dragging-indicator-bg: var(--text-color, ButtonBorder);
--multiple-dragging-text-color: Canvas;
--image-dragging-placeholder-bg: Canvas;
--image-dragging-placeholder-border: 1px GrayText solid;
@ -200,8 +218,8 @@
.viewsManagerButton {
width: auto;
color: var(--button-fg);
border-radius: 8px;
border: 1px solid var(--button-border-color);
border-radius: var(--button-border-radius, 8px);
border: 1px solid var(--views-button-border-color);
background: var(--button-bg);
&:hover {
@ -229,13 +247,17 @@
}
&.viewsCloseButton {
width: 26px;
height: 26px;
padding: 4px;
border-radius: 8px;
width: var(--size-item-large, 26px);
height: var(--size-item-large, 26px);
border-radius: var(--button-border-radius, 8px);
background: none;
display: flex;
align-items: center;
justify-content: center;
&::before {
width: 16px;
height: 16px;
mask-image: var(--close-button-icon);
}
}
@ -253,7 +275,7 @@
.viewsManagerLabel {
flex: 1 1 auto;
color: var(--text-color);
color: var(--views-text-color);
text-align: center;
height: fit-content;
width: fit-content;