@aiquants/markdown-explorer 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/LICENSE +21 -0
  3. package/README.md +588 -2
  4. package/dist/ExplorerSplit-BESGFcu6.cjs +1 -0
  5. package/dist/ExplorerSplit-DwEFmvQM.js +1362 -0
  6. package/dist/MultiLayout-DxPiARsv.js +896 -0
  7. package/dist/MultiLayout-r_qx1Mfz.cjs +1 -0
  8. package/dist/TreeLayout-9-20bbSM.cjs +1 -0
  9. package/dist/TreeLayout-CW7lgV_e.js +19 -0
  10. package/dist/client/MarkdownExplorer.d.cts +30 -0
  11. package/dist/client/MarkdownExplorer.d.ts +30 -0
  12. package/dist/client/storage.d.cts +11 -0
  13. package/dist/client/storage.d.ts +11 -0
  14. package/dist/core/config.d.cts +71 -0
  15. package/dist/core/config.d.ts +71 -0
  16. package/dist/core/contracts.d.cts +155 -0
  17. package/dist/core/contracts.d.ts +155 -0
  18. package/dist/core/geometry.d.cts +51 -0
  19. package/dist/core/geometry.d.ts +51 -0
  20. package/dist/core/labels.d.cts +175 -0
  21. package/dist/core/labels.d.ts +175 -0
  22. package/dist/core/location.d.cts +82 -0
  23. package/dist/core/location.d.ts +82 -0
  24. package/dist/core/paths.d.cts +17 -0
  25. package/dist/core/paths.d.ts +17 -0
  26. package/dist/core/revalidation.d.cts +2 -0
  27. package/dist/core/revalidation.d.ts +2 -0
  28. package/dist/core/theme.d.cts +6 -0
  29. package/dist/core/theme.d.ts +6 -0
  30. package/dist/core/treeSettings.d.cts +48 -0
  31. package/dist/core/treeSettings.d.ts +48 -0
  32. package/dist/core/viewSettings.d.cts +15 -0
  33. package/dist/core/viewSettings.d.ts +15 -0
  34. package/dist/dataRoute-Bkfwfp1R.js +194 -0
  35. package/dist/dataRoute-CKbdwkQ7.cjs +1 -0
  36. package/dist/index-BgkZ9ATV.js +2061 -0
  37. package/dist/index-y9tVvDW7.cjs +3 -0
  38. package/dist/index.cjs +1 -0
  39. package/dist/index.d.cts +12 -0
  40. package/dist/index.d.ts +12 -0
  41. package/dist/index.js +44 -0
  42. package/dist/location-Cb2Ak55B.js +382 -0
  43. package/dist/location-DWHQYntR.cjs +1 -0
  44. package/dist/sanitizeWorker.cjs +1 -0
  45. package/dist/sanitizeWorker.js +291 -0
  46. package/dist/server/anonymous.d.cts +10 -0
  47. package/dist/server/anonymous.d.ts +10 -0
  48. package/dist/server/createMarkdownExplorer.d.cts +12 -0
  49. package/dist/server/createMarkdownExplorer.d.ts +12 -0
  50. package/dist/server/errors.d.cts +26 -0
  51. package/dist/server/errors.d.ts +26 -0
  52. package/dist/server/fileSystemSource.d.cts +38 -0
  53. package/dist/server/fileSystemSource.d.ts +38 -0
  54. package/dist/server/ports.d.cts +107 -0
  55. package/dist/server/ports.d.ts +107 -0
  56. package/dist/server/sanitizeProtocol.d.cts +17 -0
  57. package/dist/server/sanitizeProtocol.d.ts +17 -0
  58. package/dist/server/sanitizer.d.cts +51 -0
  59. package/dist/server/sanitizer.d.ts +51 -0
  60. package/dist/server/workerPool.d.cts +42 -0
  61. package/dist/server/workerPool.d.ts +42 -0
  62. package/dist/server.cjs +1 -0
  63. package/dist/server.d.cts +7 -0
  64. package/dist/server.d.ts +7 -0
  65. package/dist/server.js +1253 -0
  66. package/dist/storage-Bkb2Axdi.cjs +1 -0
  67. package/dist/storage-DA4ItHzl.js +60 -0
  68. package/dist/storage.cjs +1 -0
  69. package/dist/storage.d.cts +1 -0
  70. package/dist/storage.d.ts +1 -0
  71. package/dist/storage.js +4 -0
  72. package/dist/styles/markdown-explorer.css +3 -0
  73. package/docs/behavior/dom-hooks.md +95 -0
  74. package/docs/behavior/layout.md +203 -0
  75. package/docs/behavior/localization.md +47 -0
  76. package/docs/behavior/security.md +134 -0
  77. package/docs/behavior/url-contract.md +129 -0
  78. package/package.json +141 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,92 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (unreleased)
4
+
5
+ The first release: a Markdown document explorer for React Router 8 framework mode, with a source tree beside the document, a multi-panel reading view and a full-page single view, behind server ports for authentication, storage and Markdown parsing.
6
+
7
+ ### Added
8
+
9
+ - **Three views on one URL contract**: `{basePath}/{tree}/{document}`, `{basePath}/{multi}?doc=…&maximized=…` and `{basePath}/{single}/{document}`, written only by `explorerHref` (which refuses any target the resolver would redirect or partly ignore) and read only by `resolveExplorerLocation`, with idempotent canonicalization on the server and in the browser (a client-side redirect keeps the host's frame mounted).
10
+ - **Server half** (`@aiquants/markdown-explorer/server`): `createMarkdownExplorer` returns the page loader, the page `headers` and the data route (`tree`, `children`, `document` and `refresh` answer JSON, `asset` answers a file's bytes); `createFileSystemSource` is a reference source over a folder; `anonymousAccess` serves applications without sign-in; `ExplorerError` lets a source report a reason and a host-defined code (a malformed code throws a `RangeError`).
11
+ The sources are the server's runtime setting: `createMarkdownExplorer`'s `sources` (at least one, in switcher order, the first being the default, each id accepted by the exported `isExplorerSourceId` and unique) supply the URL's `source` values, the browser reads them from the page data, and `withExplorerSources(config, sourceIds)` joins them to the shared configuration for the URL codec (`ExplorerLocationConfig`; `explorerHref` also writes source-less links, `ExplorerSourcelessHrefTarget`, from a configuration without them).
12
+ Each source declares a `boundary` (`contains`, pure and synchronous, and an optional storage `confirm` answering `{ inside: false }` or `{ inside: true, readPath }`): its tree and children list only paths inside it, a children request reaches `children(context, path, readPath)` only after the named source's `contains` and `confirm` passed, and a document reaches `document(context, path, readPath)` only when the boundary of at least one source admits it (documents carry no source; a path no source contains is `forbidden` without a host or storage call).
13
+ The tree refresh reloads the tree and answers it (200, the same body as a tree request); refreshes arriving while a walk runs share one trailing walk that begins after them, every tree carries `loadedAt` (when its walk began), sources and the document port receive `refresh: true` for a refresh and for the document requests it triggers (`X-Markdown-Explorer: refresh`), and `onTreeRefreshed(key)` with `forgetTree(key)` let a multi-process host drop the other processes' copies of a refreshed tree
14
+ (the hook may return a promise, which the response does not wait for; a throw or that promise's rejection is logged, as is the rejection of a promise a broken `contains` answers).
15
+ - **Client half**: `<MarkdownExplorer>` with per-instance stale-while-revalidate stores, streamed first paint, lazily loaded tree and multi-panel views (a single-view page never downloads them), `shouldRevalidateExplorer` for the page route, `clearExplorerStorage` for sign-out (also from the dependency-free entry `@aiquants/markdown-explorer/storage`, for sign-out handlers that run on every page), and `lockDocumentScroll`, which logs one `console.error` per mount when the explorer ends below the viewport or collapses because its container has no definite block size (it measures only rendered parts, so a collapsed explorer panel, a closed sheet or an explorer the host hides is never reported as collapsed).
16
+ The URL codec in the browser reads the shared configuration joined with the page data's source ids (`withExplorerSources`), so the browser learns the server's runtime sources from the page and no source id is built into the client. `onMathDiagnostics(path, diagnostics)` hands a markdown document's KaTeX diagnostics (`MarkdownMathDiagnostic` of `@aiquants/markdown`) to the host once the document rendered, never to the reader's console, through one stable function (a host passing a new function on every render receives each document's diagnostics once).
17
+ The tree's **Refresh** (a visible label that is also its accessible name) shows the tree the server's refresh answers, never replacing a tree with one of an earlier `loadedAt` (a seed or background answer that lands meanwhile never discards the reader's refresh; only a later refresh or a reader switch does), and then reads the documents again with the refresh header (each cached document once: the shown ones at once — one still loading as soon as its request settles —, the others when next opened), whether or not the tree reloaded;
18
+ a reloaded tree is announced (`treeRefreshed`), a failed reload keeps the tree under an alert naming the failure (`treeRefreshFailed`, `markdown-explorer-refresh-failure`), and the other sources' trees are fetched again in the background when the reader switches to them.
19
+ Children loading when a refresh fails keep loading and land. The single view has a toolbar row with its own **Refresh** for the shown document (`refreshDocument`, `documentRefreshed`, `documentRefreshFailed`; `markdown-explorer-single-toolbar`, `markdown-explorer-single-refresh`, `markdown-explorer-single-refresh-failure`).
20
+ - **Tree settings defaults**: `defaultTreeSettings` in `defineExplorerConfig` (shared by the server and the browser, so the server render and the hydration draw the same defaults) sets any of the reader's tree settings before they change one, merged over the exported package defaults `DEFAULT_TREE_SETTINGS`; an unknown key or a value the settings menu does not offer throws a `RangeError` at startup, and anything but a plain object (also for `views` and `multiView`) a `TypeError`. The types `TreeSettings`, `TreeSortMode`, `TreeSelectionMode` and `TreeDoubleClickAction` are exported for it.
21
+ Browser storage keeps only the settings the reader changed, each with the value they chose (`{ "version": 2, "settings": { … } }` under `<storageNamespace>:tree-settings`), so a later change of the host's defaults reaches every setting the reader left alone or moved back to the default, and never replaces a value the reader chose; a value of another version is ignored.
22
+ - **Lazy folders and a bounded Expand all**: Expand all opens every folder and keeps opening the folders inside as their listings land, until the tree is fully open or the press's budget runs out — `expandAll` in `defineExplorerConfig` (`maxFolders`, the listings a press may start, 1–10,000, default 500;
23
+ `maxDepth`, the deepest level, the top level being 1, at which it opens a folder whose contents are not known yet, 1–64, default 16; each defaulting on its own, a bad value or unknown key a `RangeError`, anything but a plain object a `TypeError`), with the exported limits `EXPAND_ALL_FOLDER_LIMITS` and `EXPAND_ALL_DEPTH_LIMITS`.
24
+ Folders whose contents are known always open; listings run roughly level by level; a press is announced once when it starts listing (`expandAllStarted`) and once at its end (`expandAllDone`, or `expandAllFolderLimit`, which invites another press that continues with a fresh budget, or `expandAllDepthLimit`); the button is `aria-busy` (`data-aqmx-busy`) and ignores presses while one runs; the folders a press opened join the remembered expansion, which a reload lists again as bulk work.
25
+ Lazy folders load while a tree shows them open, at most 4 listings in flight per explorer, of which bulk work (Expand all, a restored expansion, a recursive double click) uses at most 3, so a folder the reader opens or retries goes ahead of all bulk work; each source has its own queues, served in turn.
26
+ A listing no tree wants any more is cancelled at the end of the task — never sent when queued, aborted when in flight (the source's `context.signal` aborts) — and is never a failure, a Try again or an announcement: collapsing the folder or an ancestor, Collapse all (which also ends a running press, silently, at the click), a source switch, closing the sheet or collapsing the explorer panel (which pause a running press until the explorer is shown again; a hidden explorer lists nothing, also on its first render), an unmount, and a refresh, seed or reader change that replaces the tree.
27
+ A progress line floats at the bottom of the tree area without moving the tree (`markdown-explorer-tree-progress`, with `data-aqmx-loading` and `data-aqmx-expand-all`; `foldersLoading`, `expandAllProgress`), counting the source's queued and in-flight listings whichever rows are drawn.
28
+ A press stopped at a limit leaves its stop sentence in a notice above the tree instead, in the pane's flow so it covers no row (`markdown-explorer-expand-all-stop`, with `data-aqmx-stop`; not a live region), until the next press, Collapse all, the pane having no rows to show (a failed, empty or cleared tree), or unmount (a source or view switch).
29
+ A folder met again among its own ancestors (a shortcut inside its own target, or two folders holding shortcuts to each other) is a loop row, "Name (already open above)" (`loopFolderName`, `data-aqmx-loop`, no `data-aqmx-path`), that never expands and leads to the folder above, scrolling to it and moving the focus there. Try again on a failed folder that is collapsed opens it and loads it ahead of bulk work.
30
+ - **Tree row states over the connector lines**: hovered, gathered, shown and open rows are painted with translucent tints (`--aqmx-tree-row-tint`, `--aqmx-tree-row-accent-tint`), so the connector lines and expand glyphs that `@aiquants/directory-tree` draws on a canvas under the rows stay visible in every state, also on hover under forced colors.
31
+ They are drawn in `--aqmx-tree-line`, which reaches 3:1 against every row state over the tree's surface in both schemes, and in `CanvasText` under forced colors; the tree reads the colour the browser resolves for the token, so a switch between a light and a dark high-contrast theme redraws them in the new palette.
32
+ - **Golden-ratio geometry**: explorer defaults of 1/φ³ (tree) and 1/φ⁴ (multi-panel), the golden section 1/φ² as the maximum, a 13rem floor that follows the reader's root font size, an explorer sheet behind a labelled 44 px toggle on frames narrower than 13rem × φ², Fibonacci spacing and durations (every duration 0 under `prefers-reduced-motion: reduce`, the resize handle's transition and the tree's fade from `@aiquants/directory-tree` included), 40ch multi-view columns with hysteresis and an order-preserving projection when fewer columns fit.
33
+ The reader's explorer width is remembered per view from the handle's own moves only (`@aiquants/resize-panels`' layout change sources): a drag that moved it, once it ends, or an arrow key that moved it, never a press that moved nothing, a container resize or a restore. The document's column fills its pane, so the viewer's alignment control shows only for a single-view embed that narrows the column (`?width=` / `?maxWidth=`, each capped at the pane's width), and Mermaid gantt charts are drawn at their slot's width.
34
+ - **Security**: server-side DOMPurify sanitization with an allow-list, after which the document viewer's own allow list applies on every render, so the page holds the intersection (a document keeps only the converter's class tokens, and ids only on headings, footnotes and in the `user-content-` namespace; the sanitizer leaves classes and ids to the viewer's single definition instead of copying it, and a host that renders `htmlContent` elsewhere reduces them itself),
35
+ deny rules matched through up to three percent-decoding levels and after full case folding, hardened JSON responses, source boundaries (the union rule above), a same-origin custom-header requirement for the tree refresh and for the document requests it marks, and a per-reader scope that clears every browser cache when the reader changes and is checked against browser storage before anything reads it
36
+ (the host duties name what a document can still reach: credentialed same-origin GET requests through its images, media and frames without the reader's interaction and through its links once followed, and what a state-changing GET must refuse: a speculative request (`Sec-Purpose`) and any destination but a document or a same-origin script's `fetch`); a reader switch seen in a data response reloads the page data once, without cancelling a loader that is running.
37
+ The scope is a pseudonym (an HMAC of the principal key under `scopeSecret`); the sanitizer runs in worker threads with limits on size, nesting (checked by the sanitizer's own parser, parse5, as it builds the tree: an element placed deeper than 512 levels, or more elements than the input has characters, stops the parse) and time and a byte-bounded cache of outcomes,
38
+ admits at most `jobsPerScope` documents per reader (by default `multiView.maxPanels` + max(1, `workers` − 1) + 3, never less than `maxPanels`) and `maxQueuedJobs` waiting in all (by default four times `jobsPerScope`, never less than `maxPanels`), never runs one reader on every worker when there are two or more and hands a free worker to the reader running the fewest,
39
+ runs each worker in a bounded heap (`workerHeapMb`, a document that exhausts it is `too-large`) and refuses a document whose elements, attributes and comments exceed a node budget derived from the heap and `maxHtmlBytes` (81,920 by default) or that holds more than 4,096 comments (bounding jsdom's quadratic time for comments placed before `<html>` or after `</html>`), refuses before parsing any tag of more than 1,024 attributes (and `html`/`body` collecting more through repeated tags), bounds the attribute characters the parser materializes, copies included, by `maxHtmlBytes`, never answers sanitized HTML longer than four characters per byte of `maxHtmlBytes`, keeps the first outcome remembered for an input, sends a document to a worker only after the worker reports ready (so a crash while sanitizing is always cached as that document's `failed`), and never caches the failure of a worker that could not start,
40
+ keeps `data-*` only for the renderer's link, image and footnote contract, removes `data:` URLs everywhere and reduces iframes' `allow` and `referrerpolicy` to safe values; deny rules decode leniently, match every decoding level both whole and split on `\` as well as `/` (a literal `my-credentials\backup.json` is denied like `*credentials*.json`) and cover more credential files by default, every name starting with `.env` (`.envelope.md` included), and the backups and variants of `*.env` files (`*.env.*`, `*.env~*`, `*.env-*`, `*.env_*`, `*.env#*`, documents such as `deploy.env.md` included); the file-system source opens files without following links and checks they are the files it validated;
41
+ fixed-position markup stays inside the document pane.
42
+ The data route refuses a React Router single-fetch request (`<dataPath>/<operation>.data`) with a `not-found` failure before authenticating or calling a source, since React Router would answer it by reading the whole response and dropping every header but `Set-Cookie`.
43
+ A markdown document listing more than `EXPLORER_MAX_HEADINGS` (10,000, exported) headings is refused as `too-large`: every listed heading travels with the document (the data response, the browser's document cache and the table of contents), and a parser can list headings its HTML never renders.
44
+ - **Server options and types**: a required `scopeSecret` (at least 32 characters, the same on every instance); `sanitizer` (`workers`, `deadlineMs`, `maxHtmlBytes`, `cacheBytes`, `workerHeapMb`, `jobsPerScope`, `maxQueuedJobs`) and `treeCache` (`ttlMs`, `maxEntries`, `maxBytes`) limits, each defaulting on its own (`maxBytes` counts the trees' JSON;
45
+ the parsed tree kept beside each takes roughly as much again), with at most one walk of a tree at a time per cache key and no tree answered past its time to live (counted from its walk's start: a request after it waits for one new walk); tree and children entries the contract cannot carry (an empty name, a path the browser cannot address) are left out with their subtrees and logged instead of failing the listing;
46
+ the exported types `ExplorerPageLoaderResult` (named through React Router's public `data()`), `ExplorerLogger`, `ExplorerLogDetail`, `ExplorerOperation`, `ExplorerPathRules`, `HtmlSanitizerOptions`, `FileSystemSkippedEntry` and `FileSystemSkipReason`.
47
+ `createFileSystemSource` gains `walkConcurrency` (default 8), `cacheScope` (default `"principal"`) and `reportSkipped`, and previews only files with a known text extension or name.
48
+ - **Reading**: documents opened again within 55 s are used as they are and older ones revalidated in the background; nothing is fetched while React renders, so a loader value that missed its deadline renders as loading on the server and is fetched by the browser; the work behind a late tree or document keeps running for another `deferredTimeoutMs`, so the browser's data request can join it, and its `signal` then aborts (late notices, never fetched again, at once), because the page request's own signal never aborts after its response has finished; a revalidated tree or document stays on screen and is replaced only by a different value; the tree's Refresh reads documents again instead of dropping them (a shown document stays on screen and is replaced only when it changed); shown documents are never evicted from the cache; a pane never suspends after its first commit (the shown document stays with a progress line and `aria-busy`);
49
+ tree rows and document links the pointer or the focus rests on for 89 ms are prefetched one request at a time.
50
+ The default document styles and alignment are `@aiquants/markdown`'s own (`DEFAULT_MARKDOWN_STYLES`, `CONTENT_ALIGNS` and `DEFAULT_CONTENT_ALIGN`, read from its React-free `@aiquants/markdown/viewer-settings`, which the server half loads too), with no copy in the explorer.
51
+ The document viewer's table of contents makes the explorer's own jump to the heading (`onHeadingSelect`): a closed `details` around it opens, the scroll is instant, the focus moves to the heading so the next Tab continues in its section, and a page view pushes the heading's hash through the router (nothing when the URL already names it), while a panel keeps its URL;
52
+ an unpinned table of contents closes, and a modified or middle click is left to the browser: each entry links to its heading's own address (`headingHref`: the page's URL with the heading's fragment, or, in a multi-view panel, the panel's document in the tree view), so it opens the heading in a new page and a copied address reaches it.
53
+ The viewer's toolbar is placed through its `--aqmd-toolbar-inset-block-start` on the document region; where it covers the document's content the viewer reserves that band (`--aqmd-toolbar-block-overlap`) as top padding and scroll padding, and a document shown in place of another is aligned to its fragment only once its viewer has reserved the band, so the band never pushes the target down where the page or a panel scrolls.
54
+ - **Multi-panel reading**: closing or hiding the focused panel hands the focus to the next panel, the previous one or the stage; hiding, showing, maximizing and restoring are announced (a maximize or restore once the layout changed, from the header's button, Escape, a double-click or a followed link);
55
+ a panel opened or shown by a pick in the explorer (in the split or the sheet) and a restored panel are brought into the stage's view, whole and by the least distance, and kept there while the stage settles around them until the reader's next input or a scroll they did not make (assistive technology's browse mode, the find bar, a script) (a pick in the split keeps the focus on the tree row, one in the sheet returns it to the toggle; a restore keeps a focus that survived, and a link that held it is brought into view inside the panel's scroll area and kept there too);
56
+ a panel's scroll area keeps at least three lines of text below the panel's header at any width and text size (the panel grows past stage/φ when its header wraps that far, at a phone width with 200 % text or a 400 % zoom); a link to a document open in another panel shows (or, while a panel is maximized, maximizes) it;
57
+ a panel that follows a link starts at the top (or at the link's fragment);
58
+ a fragment follows one rule in every view — its element starts the pane's own scroller (a panel's scroll area whatever its range, a page view's viewer when the document scrolls in it, or, for an explorer that grows with its document, the host's scroller, the body or the page that scrolls it) while every other scroller (the stage, a host's scroller, the page, a wide table that scrolls sideways) moves only as far as needed to show it, in both axes, with the browser's own alignment (any `scroll-padding`, `min()`, `max()` and `clamp()` included,
59
+ settled at once under a host's `scroll-behavior: smooth`, and nothing moved and moved back when an element no taller than the scrollport already starts a scroller without scroll padding; one taller than the scrollport is always moved past and back, so a scroll padding is honoured for it too), after opening a closed `details` and showing a `hidden="until-found"` ancestor around the element as the browser does (an element that still has no box does not move its pane's own scroller) — and in a panel the stage then shows the element with the panel's header by the least distance,
60
+ as it shows the first heading the focus moves to after a link followed in place, so neither stays above the stage's view; a fragment-only link or a table-of-contents entry in a panel opens the panel's document at that section in the tree view in a new tab or a copied address; reading positions are kept per panel and document; a plain wheel over a long panel scrolls the stage while it can move and the panel after that, and Ctrl/⌘ + wheel scrolls the panel; `data-aqmx-visible-columns` sits on the stage; the panel-limit notice is spoken by the announcer only,
61
+ and inside the open sheet it spans the header with its whole text wrapped around the close button, which stays above it (one line covers exactly the header's band; more lines spill over the top of the sheet's body for its three seconds), without changing the header's size, so no tree row moves while it is shown or when it goes and the focused row stays visible;
62
+ panels also move without a drag, with the four move buttons of each panel's header or Alt + Shift + arrow keys (both `@aiquants/drag-drop-panels`'), mapped back like a drop, announced by the layout in the explorer's words and with the focus kept on the control that held it, and a shortcut that cannot move the panel is announced instead of being swallowed; the header's buttons show while the panel is hovered or one of them is focused, and always on a device that cannot hover; touch and pen drag a panel by a long-press on its header outside its buttons, which `@aiquants/drag-drop-panels` runs itself (the stage wires nothing for touch) and reports through the same `onColumnPanelsChange` as the move buttons, so a touch drop is mapped back, written and announced like a move, and the lift and a cancel (`touchcancel`, Escape, leaving the layout, a release where nothing would move) are announced too, while a swipe on a header or a finger on a panel's body only scrolls; a change of the visible column count keeps an image's zoom and returns the focus to the same zoom button, and focus on an element made focusable only temporarily (the heading a followed link focused) returns to that element; Zoom in and Zoom out keep the focus at the zoom limits; a panel's document links carry the router's basename in their `href`.
63
+ While a panel is maximized everything of the explorer it covers is inert (the explorer and its handle or the sheet's bar, the other panels, drag-drop-panels' list of hidden panels), so Tab never leaves it for a hidden control; the host's own chrome is the host's to keep uncovered (`--aqdd-maximize-*`, which the demo binds to its header) or make inert while a maximized panel's frame carries `data-aqmx-maximized="true"`; a restore that removes the focused element (the viewer's toolbar or table of contents) hands the focus to the panel's frame; the panel-limit notice sits in the open sheet's header (over it, never in flow, and inside the modal dialog, so presses land on it) and otherwise in the top layer at the stage's corner, above a maximized panel, or in place in browsers without the Popover API (which no longer crash the explorer at the limit), and it ends early when the focus moves onto an element it is painted over (a control painted above it, such as the sheet's close button, keeps it, so closing the sheet with that button moves the notice to the stage's corner); a panel opened or closed from the sheet is announced once the URL it wrote has rendered, so the root's region speaks it after the sheet closes, toggles the router renders as one navigation are all announced, and picking a hidden panel's document closes the sheet like any other pick.
64
+ - **Accessibility**: tree rows expose the selection through `aria-selected` (the shown document by default, the gathered documents in Multiple, the open panels in the multi-panel view) and the shown document through `aria-current="page"` (also while documents are gathered), and the tree has its own name (`treeRegion`); the tree is one Tab stop — the row last focused by the keyboard, a press or a script, else the shown document's row — which Tab reaches through `@aiquants/directory-tree`'s stand-in stop while that row is outside the render window, and the shown document's row is brought into the tree's view on load and when a link changes the shown document, without moving the focus; a panel's document region is named after its document, and the panel frame, which receives the focus in the multi view's hand-offs, is a `group` named after its document; the header's links name the view they open ("Open in the tree view", "Open in the multi-panel view"); the document region is not focusable, so the keyboard scrolls the document clicked in; controls and tree rows are one control high (34 px, 44 px under a coarse pointer), the resize handle is a 24 px band (44 px under a coarse pointer), and the header wraps instead of cutting a control off;
65
+ the failure callout offers "Try again" only for `failed` and a page reload for `unauthorized`; Reload keeps the focus while the tree reloads (`aria-disabled` and `aria-busy`, never `disabled`), Expand all and Collapse all are `aria-disabled` while unavailable (Expand all also while a press runs, with `aria-busy`), Zoom out is `aria-disabled` while a fitted image with a natural size is already at or below the smallest zoom level (1/φ² ≈ 38.2 %), following the pane's size, an image without a natural size (an SVG whose root has no absolute `width` and `height`) zooms relative to its fitted size, so Zoom in and Zoom out both act from Fit and the level is a percentage of that size, the zoom level is a polite status read when it changes,
66
+ removing or clearing gathered documents moves the focus to the next document or the tree instead of dropping it, and a failed folder's Try again moves the focus to the folder's row before the children load again; every failed load of a lazy folder's children (an expansion, a restored expansion, Expand all or a retry that fails again) is announced once through the announcer, naming the folder (`foldersLoadFailedNamed`; folders whose loads fail within one animation frame are named together in one sentence, in the place of the first), and Try again is described by the failure text; the announcer speaks every message posted within one animation frame, in order, joined as separate sentences (`announcementSequence`), never only the last, and starts empty in a new reader's explorer; it speaks through the root's live region, and through a live region inside the sheet while the sheet is open (the modal sheet hides the root's region), without speaking a message twice when the sheet opens or closes; the zoom level (`output`) and failure callouts (`role="alert"`) are live regions of their own; a failed folder's row keeps Try again wholly visible, its focus ring included (drawn inside the button), at any width, text size and language: the failure text gives way first, then the folder's name down to 3em, then Try again's text down to its icon, then the rest of the name, so in the sheet at 320 px with 200 % text, two levels deep, the icon and part of the name stay; a multi-view panel's header keeps its drag-mode toggle, Hide, Maximize and Close buttons inside the panel at a phone width with 200 % text, and its title readable: the buttons wrap below the title when the title would get less than 6em, and the custom drag mode's badge stays beside the title instead of under the buttons;
67
+ one guard at the explorer's root ignores every repeated keydown of a held Enter inside the explorer — on the controls it renders and on those other packages render inside it (the tree's rows, the panel headers' buttons, the document viewer's toolbar and its popups, the controls inside a document) — except on controls that opt in with `data-aqmx-key-repeat="allow"` (Zoom in and Zoom out, which keep stepping), and those receive only the repeats of a press that began on them while the focus stays there (a followed link or a Try again that focuses an image's Zoom out does not step it), so one press no longer runs on through the remove button, menu item, menu button, sheet control or tree row that receives the focus, and no toggle or popup flips once per repeat; a held Space toggles a tree row once;
68
+ under forced colors the shown, open and gathered rows, checked settings and open toggles are drawn in `Highlight`, focus rings (the resize handle's included) in `Highlight`, unavailable buttons in `GrayText`, and the loading cues that are otherwise only a background stay visible (the progress line in `Highlight`, every loading skeleton — the tree's, the multi stage's and a pane's — in `GrayText`);
69
+ when a followed link, a back or forward navigation, a successful Try again or a background revalidation that removes the focused element replaces the content that holds the focus, the focus moves into the replacement (its fragment's target, first heading, scroller or first control) instead of falling to the page, and a jump within the shown document (a section link in any view, a page view's link to the hash its URL already has included, or Back and Forward between two of its sections, entries the browser made itself included) moves the focus to the section without scrolling again,
70
+ also when the browser's own fragment navigation dropped it from the link to the page, so Tab and Shift+Tab continue from there, while focus the reader moved away from the document region — to an element or into a frame outside it, or to the page by pressing outside the region, also after the focus had already fallen to the page or while it was in a frame inside the region —
71
+ (and focus on a page loaded with a hash) is never taken by a later jump or by Back and Forward to another document, and focus in a frame inside the region (a PDF document, an embedded frame) counts as inside; a focused footnote reference (a link inside a superscript or subscript) shows its focus ring outside its glyph in every document style and under forced colors;
72
+ every link the explorer renders in a document keeps the attributes the viewer passes on (`title`, `lang`, `dir`, `role`, `aria-*`, `data-footnote-*`, and an `id` in the `user-content-` namespace), so a footnote back-reference is named by its `aria-label` and leads to its reference (its link, or the `sup` around it);
73
+ and every element of a document keeps the focus ring's extent as its scroll margin, so an element a fragment or the focus brings to a scroller's edge shows its whole ring,
74
+ as do tree rows (their ring drawn inside the row) and the viewer's toolbar buttons (placed a ring's extent below the clipping edge), every ring reading `--aqmx-focus-ring-width` and `--aqmx-focus-ring-offset`; a link to HTML's top of the document (`#top` in any ASCII case with no element of that id, or the empty `#`) takes a page view or a panel back to the document's start without moving the focus;
75
+ focus inside a multi-view panel returns to the same element (else the panel's frame) when a change of the visible column count or a move to another column rebuilds the panel; switching views with the header's link or the tray's Open in panels keeps the focus in the explorer (on the new view's link — the sheet's toggle on a narrow frame — or the first panel opened), whether or not the browser fires `focusout` when it removes the focused control; the tree's arrow keys, Home and End keep the focused row in view;
76
+ the content stays mounted when the frame crosses the sheet threshold, and focus on the explorer's side moves to the sheet's toggle or back to the tree; the sheet closes only on a press that started on its backdrop, and picks made while gathering documents (Multiple selection) keep it open, so the tray fills without reopening the sheet for each document; the gathered documents and the tree settings (kept in memory, saved to browser storage when it can be written) survive the explorer's move into or out of the sheet (a phone rotated), a tree drawn after the page hydrated starts from the reader's settings instead of drawing the defaults once, and focus on a tree row returns to the same row, as the tree's only Tab stop, when the frame widens past the threshold, also after a round trip (split, sheet, split); the selection tray shrinks and scrolls before the tree, which keeps at least three rows at any text size (WCAG 1.4.4).
77
+ an Escape the explorer or the document viewer handles (closing the sheet, the tree settings menu, or the viewer's table of contents, style or alignment menu) is marked handled (`preventDefault()`), which drag-drop-panels honours — also in framework mode, where React's root listener sits on the document —, so in a maximized panel the first Escape closes the menu and keeps the panel maximized and the next one restores it; a document that fails — in every view, multi-view panels included — is announced through the announcer (`documentFailed(name, title)`, spoken from the sheet while it is open, a panel's once the URL it wrote has rendered), once for each failed attempt (a Try again that fails again is announced again, while a document visited again or a panel opened again does not repeat the cached failure it shows until its new fetch settles, nor announces a cached failure no pane showed that opening it fetches again — a prefetch older than 55 s, or any failure after the tree's reload button), and its callout is no alert; a host's `renderTreeFailure` view replaces only the callout, inside the element that keeps the tree failure's hooks; the panel chrome and the stage's spacing are bound to the explorer's tokens through drag-drop-panels' `--aqdd-*` properties, so the headers' icons (3:1), titles and custom drag mode's badge (4.5:1), the restore buttons of the bar of hidden panels (4.5:1, also hovered) and the drop placeholders of a drag (4.5:1 text, also on the target, and a 3:1 dashed border against the stage) follow `theme` rather than a page's `dark` class; a panel's header keeps its title at least 6em wide and wraps the badge and buttons below it instead, every button inside the panel; and the stage scrolls in a `div`, adding no `main` landmark inside a host's own.
78
+ - **Host guidance**: [Document images](docs/behavior/url-contract.md#document-images) states how a parser emits images (a relative image's document path in `data-asset-src` and no `src`, which the explorer writes; a missing target marked with `data-asset-exists="false"` for the viewer's labelled placeholder; author-written marks stripped; and why a `src` left as written loads an explorer page);
79
+ the README lists every `!important` declaration a host cannot override, the forced-colors ones included, and the `--aqdd-*` properties the stage binds to the explorer's tokens;
80
+ the README and [Security](docs/behavior/security.md#what-the-host-must-do) require code run before the data route's functions, and every resource route of the host, to refuse React Router single-fetch requests (`.data`) before authenticating or reading; `isAcceptableDocumentPath` is exported from the server entry with `MAX_DOCUMENT_PATH_LENGTH`;
81
+ and state which attributes of a link reach the page (a footnote reference's `id` may sit on its link, `user-content-…`, or on its `sup`, `fnref:N`) and which classes and ids of a document the viewer keeps ([Security](docs/behavior/security.md#the-viewers-allow-list), with why the sanitizer does not copy that rule, and a host duty for rendering `htmlContent` anywhere else);
82
+ [DOM hooks](docs/behavior/dom-hooks.md) name the opt-in `data-aqmx-key-repeat`, place `data-aqmx-image-mode` on the image (`markdown-explorer-image`) and state which link attributes a rendered document link keeps (`data-doc-exists` only as `"false"`, no `data-doc-path` on an inert link, and the document's own `data-footnote-*`);
83
+ [Document links](docs/behavior/url-contract.md#document-links) asks no mark for links to other sites (the viewer finds them in any case, `//host` included); the README loads KaTeX's stylesheet and fonts from `@aiquants/markdown/styles/katex.min.css`, as the demo does, whose parser writes footnotes, task lists and formulas in the Go converter's (goldmark's) markup, with only the classes and ids the viewer keeps (a reference's `sup#fnref:N`, a note's `li#fn:N` in `div.footnotes`, task lists without classes, `math inline` and `math display`);
84
+ [Security](docs/behavior/security.md#what-the-host-must-do) points hosts that convert with `@aiquants/markdown`'s `parseMarkdown` to its `deniedPathSegments` (give it the explorer's deny rules) and to `@import` directives staying unexpanded unless `resolveImports` is set.
85
+ - **One asset route, served by the explorer**: the data route's `asset` operation (`<dataPath>/asset?path=<document path>[&v=<version>]`, GET and HEAD) serves every file the explorer shows or offers for download — images, audio, video and PDF in place, everything else as an attachment — admitted exactly as a document (the deny rules, a refusal logged once with the operation `asset`, then the union of the sources' boundaries),
86
+ with HEAD, ETag/304 (`If-None-Match` as RFC 9110 evaluates it: `*`, a list, the weak comparison), single byte ranges (206/416), the media-type policy, `nosniff`, a sandboxing policy except for PDF (`sandbox allow-same-origin` for inline audio and video, so a media document opened on its own refetches its file same-origin, with the reader's cookie), a `same-origin` resource policy on every 200, 206 and 304,
87
+ a `filename*` on both dispositions, a body held to its `Content-Length` (the bytes past it dropped, a stream that ends short failing the response), and `private, max-age=60, stale-while-revalidate=600, no-transform` caching with `Vary: Cookie`. `explorerAssetHref` is its only URL writer and no entry exports it.
88
+ The host supplies the bytes through the required `asset` port (`ExplorerAssetFile`: `name`, `contentType`, `size`, `version`, `open(range)`, with `ExplorerByteRange`; any regular file the document admission admits); the `document` port answers `ExplorerHostDocument`, whose image, media, text and file documents carry the file's `version` instead of a URL, and the explorer writes their `url` (an answer that still carries `url` is a logged `failed`). The sanitizer writes the `src` of every image the parser marked with `data-asset-src`.
89
+ The browser accepts the `url` of those documents only root-relative on the page's origin and in visible ASCII.
90
+ - **Files the explorer cannot show**: the `file` document kind shows, in every view, the file header over a note (the label `fileNotShown`) with a download link; a `text` document carries its file's `url` and shows the file header (Open in a new tab, Download) above its preview. `failureDocument` returns `ExplorerFailureDocument`, exported as a type.
91
+ `createFileSystemSource` requires `pathPrefix` (`""` for a source over the whole root), refuses unknown option keys, answers a file of an unknown extension (and a text file whose preview holds a NUL) as a `file` document, reports `version` (`<size>-<mtimeNs>`) for every kind but Markdown, and serves every regular file its checks admit through its `asset` port, so its deny rules are the only filter of what downloads; the port opens a file only while it keeps the device, inode, size and modification time the checks saw (`not-found` otherwise).
92
+ - **Localization**: English and Japanese catalogs with strict per-key overrides, including every control of the document viewer (`viewer…`: its toolbar, the table of contents, the style and alignment menus and each Mermaid diagram's controls, which carry no `lang` of their own), the tree's row kinds and scroll bar strings passed to `@aiquants/directory-tree` (`treeEntryKindDirectory`, `treeEntryKindFile`, `treeScrollUp`, `treeScrollDown`, `treeScrollToTop`, `treeScrollToBottom`), the tree's name (`treeRegion`), the failed-folders announcement (`foldersLoadFailedNamed`, a list of names in the language's list format), the joining of messages announced together (`announcementSequence`), the hide and show announcements (`panelHidden`, `panelShown`), and every string the multi view's `@aiquants/drag-drop-panels` layout shows, which carry no `lang` of their own: the panel headers' buttons and badge (`panelHide`, `panelMaximize`, `panelRestore`, `panelClose`, `panelSwitchToCustomDrag`, `panelSwitchToNormalDrag`, `panelCustomDragBadge`, `panelMovePreviousColumn`, `panelMoveNextColumn`, `panelMoveUp`, `panelMoveDown`), the move announcements, a touch or pen drag's lift and cancel announcements, the columns' region names (`panelMoved`, `panelMoveUnavailable`, `panelDragStarted`, `panelDragCancelled` and `multiColumnRegion`, templates whose `{name}` placeholders the layout fills, listed in `MARKDOWN_EXPLORER_LABEL_TEMPLATES` and checked when the labels are resolved), the bar of hidden panels and a hidden panel's stand-in (`hiddenPanelsTitle`, `hiddenPanelBadge`, `hiddenPanelNotice`), an empty column (`multiColumnEmpty`) and the drop placeholders (`panelDropHere`).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 fehde-k
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.