arcane-os 0.36.0 → 0.37.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.
- package/CHANGELOG.md +24 -0
- package/README.md +1 -1
- package/docs/reference/inventory/runtime-components.json +2 -2
- package/docs/reference/inventory/runtime-modules.json +4 -4
- package/docs/reference/runtime-components.md +74 -4
- package/docs/reference/runtime-modules.md +58 -9
- package/package.json +2 -2
- package/runtime/arcane/components/markdown-editor.html +171 -14
- package/runtime/arcane/entities/File.js +14 -0
- package/runtime/arcane/modules/ComponentContracts.js +2 -0
- package/runtime/arcane/modules/MD.js +148 -17
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.37.0
|
|
4
|
+
|
|
5
|
+
- Add opt-in `markdown-editor` fit layout and preview following through
|
|
6
|
+
`configure({fit:true, followPreview:true})` or `data-fit` and
|
|
7
|
+
`data-follow-preview`. Both options default to false. Fit mode keeps the
|
|
8
|
+
title, formatting controls, input, preview and save/status actions visible
|
|
9
|
+
in a bounded responsive layout with independent input and preview scrolling.
|
|
10
|
+
- Follow body edits and formatting at their rendered Markdown block without
|
|
11
|
+
moving the input or containing page. Title edits and manual preview reading
|
|
12
|
+
preserve the reader's position. Expose named CSS parts for application styling.
|
|
13
|
+
- Add `new MD(raw,{sourceMap:true})` with ordered original UTF-16 source ranges
|
|
14
|
+
and rendered comment anchors. Preserve complete raw content, ordinary default
|
|
15
|
+
rendering, formatting, save payloads, cancellation and cleanup. No saved-data
|
|
16
|
+
migration or model/provider/default changes.
|
|
17
|
+
|
|
18
|
+
## 0.36.1
|
|
19
|
+
|
|
20
|
+
- Infer ordinary audio MIME types from mp3, wav, ogg, oga, opus, aac, m4a,
|
|
21
|
+
flac and weba filenames when stored files have no supplied MIME metadata.
|
|
22
|
+
Native file previews can select audio controls without decoding audio as text.
|
|
23
|
+
- Preserve supplied MIME types, existing non-audio classification, the original
|
|
24
|
+
file and complete payload. An untyped webm remains ambiguous and retains its
|
|
25
|
+
ordinary download behavior. No stored files are rewritten.
|
|
26
|
+
|
|
3
27
|
## 0.36.0
|
|
4
28
|
|
|
5
29
|
- Add optional continuous microphone capture to `voice-transcription` through
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
|
|
|
19
19
|
`arcane/` runtime. Both profiles share the theme, packaging, event, cancellation,
|
|
20
20
|
and browser run contracts while retaining their selected application layout.
|
|
21
21
|
|
|
22
|
-
This checkout defines the `0.
|
|
22
|
+
This checkout defines the `0.37.0` SDK contract. Applications pin one exact npm
|
|
23
23
|
version and lockfile; registry state is deliberately not baked into application
|
|
24
24
|
artifacts.
|
|
25
25
|
|
|
@@ -515,7 +515,7 @@
|
|
|
515
515
|
{
|
|
516
516
|
"file": "runtime/arcane/components/markdown-editor.html",
|
|
517
517
|
"name": "markdown-editor.html",
|
|
518
|
-
"purpose": "Configurable Markdown authoring, toolbar, preview, title, and save surface.",
|
|
518
|
+
"purpose": "Configurable Markdown authoring, toolbar, preview, plain-text title, and save surface with optional bounded input/preview panes and edit-position following.",
|
|
519
519
|
"methods": [
|
|
520
520
|
"configure()",
|
|
521
521
|
"focus()",
|
|
@@ -535,7 +535,7 @@
|
|
|
535
535
|
],
|
|
536
536
|
"availability": "Browser and supported native WebViews",
|
|
537
537
|
"transport": "HTMLImport + DOM; injected Arcane/provider modules where listed",
|
|
538
|
-
"normalization": "Editor values normalized; injected save result mixed"
|
|
538
|
+
"normalization": "Editor values and optional presentation behavior normalized; injected save result mixed. fit and followPreview default false, preserve omitted prior configuration, and accept explicit false resets; data-fit and data-follow-preview select the initial options. Fit requires an application-bounded host and provides independent input/preview scrolling in roomy columns or narrow stacked panes without changing the application shell. Following responds to body typing/format edits, leaves manual preview scrolling independent until the next edit, and does not treat title edits or programmatic value replacement as follow requests. Public parts editor/title/toolbar/input/preview/actions/status/save-action allow application styling while the title stays native plain text. Existing controls, complete save payload, save cancellation, and destruction behavior remain."
|
|
539
539
|
},
|
|
540
540
|
{
|
|
541
541
|
"file": "runtime/arcane/components/media-embed.html",
|
|
@@ -417,7 +417,7 @@
|
|
|
417
417
|
"availability": "Cross-host with an injected event constructor outside DOM hosts",
|
|
418
418
|
"protocol": "In-process only",
|
|
419
419
|
"normalization": "Fully normalized labels, rows, definitions, visibility, formats, editor and voice options, plus capability-neutral STT activation intent and presentation state. Complete finite progress measures remain visible, including fractional and over-total values.",
|
|
420
|
-
"surface": "Constant sets plus normalization, complete finite-progress formatting, and explicit STT activation helpers. normalizeVoiceOptions preserves manual capture and standard controls by default and accepts continuous capture timing/activity options, simple controls, showComplete, and retry/cancel labels."
|
|
420
|
+
"surface": "Constant sets plus normalization, complete finite-progress formatting, and explicit STT activation helpers. normalizeVoiceOptions preserves manual capture and standard controls by default and accepts continuous capture timing/activity options, simple controls, showComplete, and retry/cancel labels. normalizeMarkdownOptions adds fit and followPreview strict booleans default false, retains omitted prior values, and accepts explicit false resets without changing existing editor options."
|
|
421
421
|
},
|
|
422
422
|
{
|
|
423
423
|
"file": "runtime/arcane/modules/ConfiguredAIChatSession.js",
|
|
@@ -885,11 +885,11 @@
|
|
|
885
885
|
"exports": [
|
|
886
886
|
"default"
|
|
887
887
|
],
|
|
888
|
-
"summary": "Renders complete Markdown with Marked and
|
|
888
|
+
"summary": "Renders complete Markdown with Marked and optionally maps source blocks to rendered comment anchors.",
|
|
889
889
|
"availability": "Browser / native WebView",
|
|
890
890
|
"protocol": "Marked",
|
|
891
|
-
"normalization": "Complete raw and rendered Marked values; parse errors remain vendor-native.",
|
|
892
|
-
"surface": "default
|
|
891
|
+
"normalization": "Complete raw and rendered Marked values; parse errors remain vendor-native. Opt-in source mapping preserves original UTF-16 offsets and adds only comment anchors to complete rendered output. Lists, quotes, and tables map at top-level block granularity; invisible source or unavailable parsed anchors use the consumer's nearest available block behavior. Mapping is rebuilt on raw replacement and append, without persistence or viewport movement.",
|
|
892
|
+
"surface": "default MD; new MD(raw,{sourceMap:true}) enables ordered sourceMap records {start,end,marker,type}, with inclusive start/exclusive end in original UTF-16 source positions and marker equal to generated comment data. Locate anchors with a comment TreeWalker, not a CSS selector. Markers are opaque and may change after edits. sourceMap exposes the current mutable array, replaced after each successful render. raw, rendered, safeRendered, and append() remain; default sourceMap is empty and ordinary output is unchanged. Rendering and getters are DOM-independent."
|
|
893
893
|
},
|
|
894
894
|
{
|
|
895
895
|
"file": "runtime/arcane/modules/MemoryRecords.js",
|
|
@@ -77,7 +77,7 @@ appropriate.
|
|
|
77
77
|
| [`integration-settings.html`](#integration-settingshtml) | Edits non-secret communication service configuration and service actions. | `configure()`<br>`getValues()`<br>`setStatus()`<br>`destroy()` | `integration-settings-ready`<br>`integration-settings-save`<br>`integration-settings-close`<br>`integration-action` | Normalized non-secret values |
|
|
78
78
|
| [`local-ai-status.html`](#local-ai-statushtml) | Presents local-AI standby, failure, recovery, guidance, retry, and dismissal states. | `configure()`<br>`begin()`<br>`present()`<br>`destroy()`<br>`hidden` | `local-ai-status-ready`<br>`local-ai-status-dismissed`<br>`local-ai-retry` | Fully normalized LocalAIReadiness report |
|
|
79
79
|
| [`markdown-document.html`](#markdown-documenthtml) | Renders and navigates a complete Markdown document with focusable fragments. | `configure()`<br>`load()`<br>`render()`<br>`clear()`<br>`fail()`<br>`focus()`<br>`focusFragment()`<br>`destroy()` | `markdown-document-ready`<br>`markdown-document-state`<br>`markdown-document-loading`<br>`markdown-document-rendered`<br>`markdown-document-empty`<br>`markdown-document-error`<br>`markdown-document-navigate` | Complete Markdown/state normalized; malformed input and Marked/DOM failures remain visible |
|
|
80
|
-
| [`markdown-editor.html`](#markdown-editorhtml) | Configurable Markdown authoring
|
|
80
|
+
| [`markdown-editor.html`](#markdown-editorhtml) | Configurable Markdown authoring with optional bounded input/preview panes and edit-position following. | `configure()`<br>`focus()`<br>`clear()`<br>`saveEntry()`<br>`destroy()` | `markdown-editor-ready`<br>`markdown-editor-change`<br>`markdown-editor-saved` | Editor values, opt-in fit layout, and preview following normalized; injected save result mixed |
|
|
81
81
|
| [`media-embed.html`](#media-embedhtml) | Loads a parsed YouTube video or playlist embed with ordinary hosting by default, optional privacy enhancement, and an external-platform action. | `configure()`<br>`load()`<br>`destroy()` | `media-embed-ready`<br>`media-load`<br>`media-error`<br>`media-open-platform` | URL/error normalized; iframe/platform behavior native |
|
|
82
82
|
| [`modal.html`](#modalhtml) | Generic modal with population, configurable user dismissal, actions, and concurrent task execution. | `configure()`<br>`populate()`<br>`open()`<br>`close()`<br>`runTasks()`<br>`destroy()`<br>`running`<br>`opened` | `modal-ready`<br>`modal-opened`<br>`modal-closed`<br>`modal-action` | Modal state normalized; injected task results mixed |
|
|
83
83
|
| [`output-panel.html`](#output-panelhtml) | Presents status, output, body, coverage, actions, pending, error, and cleared states. | `configure()`<br>`setOutput()`<br>`setBody()`<br>`setCoverage()`<br>`setActions()`<br>`setPending()`<br>`setStatus()`<br>`setError()`<br>`clear()`<br>`destroy()` | `output-panel-ready`<br>`output-panel-state`<br>`output-panel-change`<br>`output-panel-action`<br>`output-panel-error`<br>`output-panel-cleared` | DOM-normalized |
|
|
@@ -1021,11 +1021,56 @@ subject to the bound app's capabilities. [Deep protocol details](protocols.md).
|
|
|
1021
1021
|
|
|
1022
1022
|
### Overview
|
|
1023
1023
|
|
|
1024
|
-
Configurable Markdown authoring, toolbar, preview, title, and save
|
|
1024
|
+
Configurable Markdown authoring, toolbar, preview, plain-text title, and save
|
|
1025
|
+
surface. The default editor retains its growing stacked layout and existing
|
|
1026
|
+
controls. Optional fit mode keeps the Markdown input and rendered preview
|
|
1027
|
+
inside an application-supplied bounded area.
|
|
1025
1028
|
|
|
1026
1029
|
### Public surface
|
|
1027
1030
|
|
|
1028
|
-
Methods/properties: `configure()`, `focus()`, `clear()`, `saveEntry()`,
|
|
1031
|
+
Methods/properties: `configure()`, `focus()`, `clear()`, `saveEntry()`,
|
|
1032
|
+
`destroy()`, `value`, `entryTitle`, `options`.
|
|
1033
|
+
|
|
1034
|
+
`configure({fit:true,followPreview:true})` opts into the two independent
|
|
1035
|
+
behaviors. Both options default to `false`; omitted options retain their
|
|
1036
|
+
previous values during reconfiguration, and explicit `false` disables either.
|
|
1037
|
+
Initial markup can use `data-fit="true"` and `data-follow-preview="true"`.
|
|
1038
|
+
|
|
1039
|
+
`fit:true` fills the host's available height. The application must supply a
|
|
1040
|
+
bounded host height and allow its enclosing flex/grid item to shrink, for
|
|
1041
|
+
example with `min-height:0`. Input and preview have independent scroll areas:
|
|
1042
|
+
two columns when room permits and two bounded stacked panes on narrow widths.
|
|
1043
|
+
Title, toolbar, status, and save remain outside those scrolling panes. Hiding
|
|
1044
|
+
the preview leaves the input the available pane space. Fit mode does not
|
|
1045
|
+
change the application's fixed shell or scroll the page.
|
|
1046
|
+
|
|
1047
|
+
`followPreview:true` follows the Markdown body's active edit position after
|
|
1048
|
+
typing or applying a toolbar format. It scrolls the preview only as needed
|
|
1049
|
+
to bring a short corresponding block into view; a block taller than the pane
|
|
1050
|
+
uses the caret's approximate fraction within that block. Users can scroll the
|
|
1051
|
+
preview independently, and rerendering retains that reading position; the next
|
|
1052
|
+
body edit may reposition it. Title edits,
|
|
1053
|
+
programmatic value replacement, and ordinary preview scrolling do not request
|
|
1054
|
+
edit-position following. Moving the selection alone does not trigger following,
|
|
1055
|
+
and following never scrolls the input pane or containing page. The title
|
|
1056
|
+
remains a native plain-text input.
|
|
1057
|
+
Following uses the shared [Markdown block mapping](runtime-modules.md#mdjs);
|
|
1058
|
+
lists, quotes, and tables follow their block, while invisible source positions
|
|
1059
|
+
use a nearby available rendered block rather than exact glyph geometry.
|
|
1060
|
+
|
|
1061
|
+
Public CSS parts are `editor`, `title`, `toolbar`, `input`, `preview`,
|
|
1062
|
+
`actions`, `status`, and `save-action`. Applications can make the title
|
|
1063
|
+
borderless and compact through `::part(title)` while retaining its native
|
|
1064
|
+
editing behavior and visible keyboard focus. These parts are styling hooks,
|
|
1065
|
+
not slots or replacement controls.
|
|
1066
|
+
|
|
1067
|
+
Existing `showTitle`, `showToolbar`, `showSave`, `showPreview`, `readOnly`,
|
|
1068
|
+
`clearOnSave`, labels, formats, and callbacks keep their behavior. `onSave`
|
|
1069
|
+
or the host's `save` receives complete `{title,markdown}` plus a second
|
|
1070
|
+
`{signal,operationId}` context; layout and following add no fields to the
|
|
1071
|
+
saved content. Edits, replacement, clear, and destruction retain the existing
|
|
1072
|
+
save cancellation and late-result suppression. Destruction also cancels the
|
|
1073
|
+
pending preview frame.
|
|
1029
1074
|
|
|
1030
1075
|
Events: `markdown-editor-ready`, `markdown-editor-change`, `markdown-editor-saved`.
|
|
1031
1076
|
|
|
@@ -1033,7 +1078,10 @@ Shared dependencies: [`MD.js`](runtime-modules.md#mdjs), [`ComponentContracts.js
|
|
|
1033
1078
|
|
|
1034
1079
|
### Availability and normalization
|
|
1035
1080
|
|
|
1036
|
-
**Browser and supported native WebViews.** Editor values
|
|
1081
|
+
**Browser and supported native WebViews.** Editor values, opt-in fit layout,
|
|
1082
|
+
and preview following are normalized; the injected save result remains
|
|
1083
|
+
application-owned. HTMLImport + DOM; native methods remain subject to the
|
|
1084
|
+
bound app's capabilities. [Deep protocol details](protocols.md).
|
|
1037
1085
|
|
|
1038
1086
|
### Example
|
|
1039
1087
|
|
|
@@ -1044,6 +1092,28 @@ Shared dependencies: [`MD.js`](runtime-modules.md#mdjs), [`ComponentContracts.js
|
|
|
1044
1092
|
</html-import>
|
|
1045
1093
|
```
|
|
1046
1094
|
|
|
1095
|
+
For a bounded editor, choose the height in the application and style only the
|
|
1096
|
+
public title part:
|
|
1097
|
+
|
|
1098
|
+
```html
|
|
1099
|
+
<style>
|
|
1100
|
+
#field-notes { display:block; height:32rem; min-height:0; }
|
|
1101
|
+
#field-notes::part(title) {
|
|
1102
|
+
border:0;
|
|
1103
|
+
border-radius:0;
|
|
1104
|
+
background:transparent;
|
|
1105
|
+
padding:.35em 0;
|
|
1106
|
+
}
|
|
1107
|
+
</style>
|
|
1108
|
+
<html-import
|
|
1109
|
+
id="field-notes"
|
|
1110
|
+
href="/arcane/components/markdown-editor.html"
|
|
1111
|
+
data-fit="true"
|
|
1112
|
+
data-follow-preview="true"
|
|
1113
|
+
data-title-placeholder="Dragon field notes">
|
|
1114
|
+
</html-import>
|
|
1115
|
+
```
|
|
1116
|
+
|
|
1047
1117
|
## media-embed.html
|
|
1048
1118
|
|
|
1049
1119
|
### Overview
|
|
@@ -98,7 +98,7 @@ own asynchronous work, cancellation, and backpressure.
|
|
|
98
98
|
| [`MailTransport.mjs`](#mailtransportmjs) | esm | Sends one complete mail report to a normalized HTTP(S) endpoint. | Browser/server with fetch + cloud | Normalized endpoint and transport errors; remote detail preserved. |
|
|
99
99
|
| [`MarkdownSpeech.js`](#markdownspeechjs) | esm | Removes repeated Markdown formatting marks from streamed narration. | Cross-host | Speech-only filtering; single marks and ordinary punctuation remain literal. |
|
|
100
100
|
| [`Marked.min.js`](#markedminjs) | esm | Vendored Marked 18.0.5 Markdown lexer, parser, renderer, extension, and walk-token API. | Cross-host vendor module | Vendor-native Marked contract. |
|
|
101
|
-
| [`MD.js`](#mdjs) | esm | Renders complete Markdown with Marked and
|
|
101
|
+
| [`MD.js`](#mdjs) | esm | Renders complete Markdown with Marked and optionally maps source blocks to rendered comment anchors. | Browser / native WebView | Complete raw and rendered Marked values; opt-in original-source offsets; parse errors vendor-native. |
|
|
102
102
|
| [`MemoryRecords.js`](#memoryrecordsjs) | esm | Normalizes memory content and detects meaningful stored memory. | Cross-host | Fully normalized string/boolean results. |
|
|
103
103
|
| [`MessageAdvisory.js`](#messageadvisoryjs) | esm | Normalizes message content advisories and contains per-message inspection failures. | Cross-host | Normalized advisory records; inspector failures converted to unavailable results. |
|
|
104
104
|
| [`ModelDefinition.js`](#modeldefinitionjs) | esm | Parses the deterministic packaged Modelfile subset and extracts the SYSTEM prompt. | Cross-host | Complete mutable definition with coded malformed-input errors. |
|
|
@@ -1647,6 +1647,14 @@ the shared voice component. Positive quiet/chunk durations and nonnegative
|
|
|
1647
1647
|
pre-roll/activity values must be finite. Complete callback payloads remain
|
|
1648
1648
|
owned by the component and application.
|
|
1649
1649
|
|
|
1650
|
+
`normalizeMarkdownOptions(input,previous)` adds strict opt-in `fit` and
|
|
1651
|
+
`followPreview` booleans, both defaulting to `false`. Omitted fields preserve
|
|
1652
|
+
the corresponding previous value; explicit `false` disables the option.
|
|
1653
|
+
Existing Markdown labels, formats, visibility, read-only state, save behavior,
|
|
1654
|
+
callbacks, complete initial Markdown, and plain initial title remain intact.
|
|
1655
|
+
The shared editor consumes these options for bounded responsive panes and
|
|
1656
|
+
body-edit preview following; the normalizer itself performs no DOM work.
|
|
1657
|
+
|
|
1650
1658
|
`createSTTActivationController({host,button,progress=null,onChange,EventClass=CustomEvent})`
|
|
1651
1659
|
consumes only normalized
|
|
1652
1660
|
[`AIRuntimeState`](#airuntimestatejs) `stt` role records. Its mutable controller
|
|
@@ -2759,25 +2767,66 @@ console.log(Object.keys(module));
|
|
|
2759
2767
|
### Overview
|
|
2760
2768
|
|
|
2761
2769
|
Renders complete Markdown with Marked and exposes the same complete rendered
|
|
2762
|
-
markup through `rendered` and `safeRendered`.
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2770
|
+
markup through `rendered` and `safeRendered`. Optional source mapping lets a
|
|
2771
|
+
consumer locate rendered blocks without adding visible wrappers or changing
|
|
2772
|
+
the authored Markdown.
|
|
2773
|
+
|
|
2774
|
+
### Public surface
|
|
2775
|
+
|
|
2776
|
+
Default `MD`; `raw`, `rendered`, `safeRendered`, `sourceMap`, `append()`.
|
|
2777
|
+
|
|
2778
|
+
`new MD(raw,{sourceMap:true})` enables block mapping. The `sourceMap` getter
|
|
2779
|
+
returns ordered `{start,end,marker,type}` records. `start` is inclusive and
|
|
2780
|
+
`end` is exclusive, measured in UTF-16 positions in the exact original `raw`
|
|
2781
|
+
string, matching native textarea selection offsets. `marker` is the complete
|
|
2782
|
+
data of a generated HTML comment, not a CSS selector. A consumer can locate
|
|
2783
|
+
it after rendering with `document.createTreeWalker(root,NodeFilter.SHOW_COMMENT)`.
|
|
2784
|
+
The generated marker prefix is chosen so it does not collide with authored
|
|
2785
|
+
source comments. Marker values are opaque and may change after an edit.
|
|
2786
|
+
The getter exposes the current mutable array; each successful render replaces
|
|
2787
|
+
it with a new array, so a previously retained array describes the prior render.
|
|
2788
|
+
|
|
2789
|
+
In this opt-in mode, `rendered` and `safeRendered` contain the complete rendered
|
|
2790
|
+
Markdown plus comment anchors; no element wrappers or attributes are added.
|
|
2791
|
+
Lists, block quotes, and tables map at their top-level block rather than to
|
|
2792
|
+
individual characters. Blank space and definitions have no visible block;
|
|
2793
|
+
raw HTML can also absorb a comment during DOM parsing. A consumer follows the
|
|
2794
|
+
nearest available rendered block in those cases. This is block-position
|
|
2795
|
+
mapping, not exact rendered glyph or caret geometry.
|
|
2796
|
+
|
|
2797
|
+
Assigning `raw` or calling `append(string)` rebuilds both the complete rendered
|
|
2798
|
+
result and its current mapping. Without `sourceMap:true`, `sourceMap` is empty
|
|
2799
|
+
and the ordinary rendered output is unchanged. Existing raw content, link
|
|
2800
|
+
behavior, no-op `rendered` setter, and complete `append()` result remain.
|
|
2767
2801
|
|
|
2768
2802
|
Exact exports: `default`.
|
|
2769
2803
|
|
|
2770
2804
|
### Availability and normalization
|
|
2771
2805
|
|
|
2772
2806
|
**Browser / native WebView.** Raw Marked behavior is preserved; parse errors are
|
|
2773
|
-
vendor-native.
|
|
2807
|
+
vendor-native. Rendering and the getters are DOM-independent. DOM lookup and
|
|
2808
|
+
scrolling belong to the consuming component;
|
|
2809
|
+
mapping performs no persistence or viewport movement. Transport: Marked.
|
|
2810
|
+
[Deep protocol details](protocols.md).
|
|
2774
2811
|
|
|
2775
2812
|
### Example
|
|
2776
2813
|
|
|
2777
2814
|
```javascript
|
|
2778
|
-
import
|
|
2815
|
+
import MD from '/arcane/modules/MD.js';
|
|
2779
2816
|
|
|
2780
|
-
|
|
2817
|
+
const notes=new MD('# Dragon field notes\n\nThe library dragon ate the index.',{
|
|
2818
|
+
sourceMap:true
|
|
2819
|
+
});
|
|
2820
|
+
const preview=document.querySelector('#preview');
|
|
2821
|
+
preview.innerHTML=notes.safeRendered;
|
|
2822
|
+
const comments=document.createTreeWalker(preview,NodeFilter.SHOW_COMMENT);
|
|
2823
|
+
const firstBlock=notes.sourceMap[0];
|
|
2824
|
+
while(comments.nextNode()){
|
|
2825
|
+
if(comments.currentNode.data===firstBlock.marker){
|
|
2826
|
+
console.log(firstBlock,comments.currentNode.nextSibling);
|
|
2827
|
+
break;
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2781
2830
|
```
|
|
2782
2831
|
|
|
2783
2832
|
## MemoryRecords.js
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arcane-os",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
4
4
|
"description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.mjs",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
|
|
87
87
|
"test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
|
|
88
88
|
"test:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.test.mjs test/app-selection.test.mjs test/contracts.test.mjs test/doctor.test.mjs test/mail-credentials.test.mjs test/mail-outbox.test.mjs test/mail-public-api.test.mjs test/mail-send.test.mjs test/mail-transport.test.mjs test/targets.test.mjs test/workspace-operation-lock.test.mjs",
|
|
89
|
-
"test:functional": "node ./bin/arcane-test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/continuous-voice-capture.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/file-preview.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/modal.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/twin-cloud.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
|
|
89
|
+
"test:functional": "node ./bin/arcane-test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/continuous-voice-capture.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/file-preview.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/markdown-editor.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/twin-cloud.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
|
|
90
90
|
"test:integration": "node ./bin/arcane-test.mjs test/installed-package-runtime.test.mjs test/root-app-layout.test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-plan.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
|
|
91
91
|
"test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/html-import-registration.test.mjs test/logging-regression.test.mjs test/markdown-speech.test.mjs test/prepared-speech.test.mjs test/native-provider-generation.test.mjs test/speech-queue-regression.test.mjs test/testing.test.mjs test/test-sets.test.mjs",
|
|
92
92
|
"check": "node tools/check-source.mjs && npm test",
|
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
display:block;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
:host([data-fit="true"]){
|
|
8
|
+
min-width:0;
|
|
9
|
+
min-height:0;
|
|
10
|
+
container:arcane-markdown-editor / inline-size;
|
|
11
|
+
}
|
|
12
|
+
|
|
7
13
|
.markdown-editor{
|
|
8
14
|
display:grid;
|
|
9
15
|
gap:.75em;
|
|
@@ -57,19 +63,62 @@
|
|
|
57
63
|
margin-right:auto;
|
|
58
64
|
}
|
|
59
65
|
|
|
66
|
+
.markdown-editor.fit{
|
|
67
|
+
height:100%;
|
|
68
|
+
min-height:0;
|
|
69
|
+
min-width:0;
|
|
70
|
+
grid-template-columns:minmax(0,1fr) minmax(0,1fr);
|
|
71
|
+
grid-template-rows:auto auto minmax(0,1fr) auto;
|
|
72
|
+
grid-template-areas:"title title" "toolbar toolbar" "input preview" "actions actions";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.fit .entry-title{grid-area:title;}
|
|
76
|
+
.fit .toolbar{grid-area:toolbar;}
|
|
77
|
+
.fit .entry-markdown{grid-area:input;}
|
|
78
|
+
.fit .preview{grid-area:preview;}
|
|
79
|
+
.fit .actions{grid-area:actions;}
|
|
80
|
+
|
|
81
|
+
.fit .entry-markdown,
|
|
82
|
+
.fit .preview{
|
|
83
|
+
box-sizing:border-box;
|
|
84
|
+
min-width:0;
|
|
85
|
+
min-height:0;
|
|
86
|
+
height:100%;
|
|
87
|
+
overflow:auto;
|
|
88
|
+
overscroll-behavior:contain;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.fit .entry-markdown{resize:none;}
|
|
92
|
+
.fit .preview{overflow-wrap:anywhere;}
|
|
93
|
+
.fit .preview img{max-width:100%;height:auto;}
|
|
94
|
+
.fit.without-preview{grid-template-areas:"title title" "toolbar toolbar" "input input" "actions actions";}
|
|
95
|
+
|
|
96
|
+
@container arcane-markdown-editor (max-width:44rem){
|
|
97
|
+
.markdown-editor.fit{
|
|
98
|
+
grid-template-columns:minmax(0,1fr);
|
|
99
|
+
grid-template-rows:auto auto minmax(0,1fr) minmax(0,1fr) auto;
|
|
100
|
+
grid-template-areas:"title" "toolbar" "input" "preview" "actions";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.markdown-editor.fit.without-preview{
|
|
104
|
+
grid-template-rows:auto auto minmax(0,1fr) auto;
|
|
105
|
+
grid-template-areas:"title" "toolbar" "input" "actions";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
60
109
|
.hidden{
|
|
61
110
|
display:none;
|
|
62
111
|
}
|
|
63
112
|
</style>
|
|
64
113
|
|
|
65
|
-
<section class="markdown-editor">
|
|
66
|
-
<input id="entryTitle" class="entry-title" type="text">
|
|
67
|
-
<section id="preview" class="preview"></section>
|
|
68
|
-
<section id="toolbar" class="toolbar"></section>
|
|
69
|
-
<textarea id="entryMarkdown" class="entry-markdown"></textarea>
|
|
70
|
-
<section class="actions">
|
|
71
|
-
<span id="status" class="status" aria-live="polite"></span>
|
|
72
|
-
<button id="save" type="button" disabled></button>
|
|
114
|
+
<section class="markdown-editor" part="editor">
|
|
115
|
+
<input id="entryTitle" class="entry-title" part="title" type="text">
|
|
116
|
+
<section id="preview" class="preview" part="preview" tabindex="0"></section>
|
|
117
|
+
<section id="toolbar" class="toolbar" part="toolbar"></section>
|
|
118
|
+
<textarea id="entryMarkdown" class="entry-markdown" part="input"></textarea>
|
|
119
|
+
<section class="actions" part="actions">
|
|
120
|
+
<span id="status" class="status" part="status" aria-live="polite"></span>
|
|
121
|
+
<button id="save" part="save-action" type="button" disabled></button>
|
|
73
122
|
</section>
|
|
74
123
|
</section>
|
|
75
124
|
|
|
@@ -88,6 +137,7 @@
|
|
|
88
137
|
const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
|
|
89
138
|
const events=createArcaneEventSource(host,{source:'arcane.component.markdown-editor',eventTypes:['markdown-editor-change','markdown-editor-saved','markdown-editor-ready']});
|
|
90
139
|
const lifecycleController=new AbortController();
|
|
140
|
+
const editor=host.shadowRoot.querySelector('.markdown-editor');
|
|
91
141
|
const titleInput=host.shadowRoot.querySelector('#entryTitle');
|
|
92
142
|
const preview=host.shadowRoot.querySelector('#preview');
|
|
93
143
|
const toolbar=host.shadowRoot.querySelector('#toolbar');
|
|
@@ -96,6 +146,7 @@
|
|
|
96
146
|
const status=host.shadowRoot.querySelector('#status');
|
|
97
147
|
|
|
98
148
|
let previewFrame=0;
|
|
149
|
+
let previewFollowPending=false;
|
|
99
150
|
let operationSequence=0;
|
|
100
151
|
let saveGeneration=0;
|
|
101
152
|
let saveController=null;
|
|
@@ -133,6 +184,9 @@
|
|
|
133
184
|
titleInput.addEventListener('input',contentChanged,{signal:lifecycleController.signal});
|
|
134
185
|
toolbar.addEventListener('click',formatMarkdown,{signal:lifecycleController.signal});
|
|
135
186
|
saveButton.addEventListener('click',saveEntry,{signal:lifecycleController.signal});
|
|
187
|
+
for(const type of ['wheel','pointerdown','touchstart','keydown']){
|
|
188
|
+
preview.addEventListener(type,retainPreviewReadingPosition,{signal:lifecycleController.signal,passive:true});
|
|
189
|
+
}
|
|
136
190
|
|
|
137
191
|
function optionsFromDataset(){
|
|
138
192
|
const dataset=host.dataset||{};
|
|
@@ -151,6 +205,8 @@
|
|
|
151
205
|
}
|
|
152
206
|
for(const field of [
|
|
153
207
|
'clearOnSave',
|
|
208
|
+
'fit',
|
|
209
|
+
'followPreview',
|
|
154
210
|
'readOnly',
|
|
155
211
|
'showPreview',
|
|
156
212
|
'showSave',
|
|
@@ -191,8 +247,21 @@
|
|
|
191
247
|
}
|
|
192
248
|
|
|
193
249
|
function renderOptions(){
|
|
250
|
+
host.dataset.fit=String(options.fit);
|
|
251
|
+
editor.classList.toggle('fit',options.fit);
|
|
252
|
+
editor.classList.toggle('without-preview',!options.showPreview);
|
|
253
|
+
// Keyboard order follows the displayed order in each layout.
|
|
254
|
+
if(options.fit){
|
|
255
|
+
editor.insertBefore(toolbar,preview);
|
|
256
|
+
editor.insertBefore(textarea,preview);
|
|
257
|
+
}else{
|
|
258
|
+
editor.insertBefore(preview,toolbar);
|
|
259
|
+
editor.insertBefore(textarea,toolbar.nextSibling);
|
|
260
|
+
}
|
|
194
261
|
titleInput.placeholder=options.labels.titlePlaceholder;
|
|
262
|
+
titleInput.setAttribute('aria-label',options.labels.titlePlaceholder);
|
|
195
263
|
textarea.placeholder=options.labels.bodyPlaceholder;
|
|
264
|
+
textarea.setAttribute('aria-label',options.labels.bodyPlaceholder);
|
|
196
265
|
preview.setAttribute('aria-label',options.labels.preview);
|
|
197
266
|
preview.dataset.emptyLabel=options.labels.preview;
|
|
198
267
|
toolbar.setAttribute('aria-label',options.labels.toolbar);
|
|
@@ -221,12 +290,14 @@
|
|
|
221
290
|
toolbar.replaceChildren(fragment);
|
|
222
291
|
}
|
|
223
292
|
|
|
224
|
-
function contentChanged(){
|
|
293
|
+
function contentChanged(event){
|
|
225
294
|
if(destroyed){
|
|
226
295
|
return;
|
|
227
296
|
}
|
|
228
297
|
supersedeSave('markdown-content-edited');
|
|
229
|
-
|
|
298
|
+
if(event?.currentTarget!==titleInput){
|
|
299
|
+
schedulePreview(options.followPreview);
|
|
300
|
+
}
|
|
230
301
|
const detail={
|
|
231
302
|
markdown:textarea.value,
|
|
232
303
|
title:titleInput.value
|
|
@@ -239,17 +310,28 @@
|
|
|
239
310
|
}
|
|
240
311
|
}
|
|
241
312
|
|
|
242
|
-
function schedulePreview(){
|
|
313
|
+
function schedulePreview(follow=false){
|
|
243
314
|
if(destroyed){
|
|
244
315
|
return;
|
|
245
316
|
}
|
|
246
317
|
if(previewFrame){
|
|
247
318
|
cancelAnimationFrame(previewFrame);
|
|
248
319
|
}
|
|
249
|
-
|
|
320
|
+
previewFollowPending=previewFollowPending||follow;
|
|
321
|
+
previewFrame=requestAnimationFrame(renderPendingPreview);
|
|
250
322
|
}
|
|
251
323
|
|
|
252
|
-
function
|
|
324
|
+
function renderPendingPreview(){
|
|
325
|
+
const follow=previewFollowPending;
|
|
326
|
+
previewFrame=0;
|
|
327
|
+
updatePreview(follow);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function retainPreviewReadingPosition(){
|
|
331
|
+
previewFollowPending=false;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function updatePreview(follow=false){
|
|
253
335
|
if(destroyed){
|
|
254
336
|
return;
|
|
255
337
|
}
|
|
@@ -257,13 +339,87 @@
|
|
|
257
339
|
cancelAnimationFrame(previewFrame);
|
|
258
340
|
}
|
|
259
341
|
previewFrame=0;
|
|
260
|
-
|
|
342
|
+
previewFollowPending=false;
|
|
343
|
+
const scrollTop=preview.scrollTop;
|
|
344
|
+
const scrollLeft=preview.scrollLeft;
|
|
345
|
+
const markdown=new MD(textarea.value,{sourceMap:options.followPreview});
|
|
346
|
+
preview.innerHTML=markdown.safeRendered;
|
|
261
347
|
preview.querySelectorAll('a').forEach(
|
|
262
348
|
link=>{
|
|
263
349
|
link.target='_blank';
|
|
264
350
|
link.removeAttribute('rel');
|
|
265
351
|
}
|
|
266
352
|
);
|
|
353
|
+
preview.scrollTop=scrollTop;
|
|
354
|
+
preview.scrollLeft=scrollLeft;
|
|
355
|
+
if(follow&&options.followPreview&&options.showPreview){
|
|
356
|
+
followTypingPosition(markdown.sourceMap);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function followTypingPosition(sourceMap){
|
|
361
|
+
const records=new Map(sourceMap.map(record=>[record.marker,record]));
|
|
362
|
+
const walker=document.createTreeWalker(preview,NodeFilter.SHOW_COMMENT);
|
|
363
|
+
const anchors=[];
|
|
364
|
+
for(let node=walker.nextNode();node;node=walker.nextNode()){
|
|
365
|
+
const record=records.get(node.data);
|
|
366
|
+
if(record){
|
|
367
|
+
anchors.push({node,record});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if(!anchors.length||!preview.clientHeight){
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const caret=textarea.selectionDirection==='backward'
|
|
374
|
+
?textarea.selectionStart:textarea.selectionEnd;
|
|
375
|
+
let selected=0;
|
|
376
|
+
for(let index=0;index<anchors.length;index+=1){
|
|
377
|
+
if(anchors[index].record.start>caret){
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
selected=index;
|
|
381
|
+
}
|
|
382
|
+
// Raw HTML can absorb an anchor or have no visible geometry. Use the
|
|
383
|
+
// nearest rendered block; never move the containing page or input pane.
|
|
384
|
+
let left=selected;
|
|
385
|
+
let right=selected+1;
|
|
386
|
+
while(left>=0||right<anchors.length){
|
|
387
|
+
const useLeft=right>=anchors.length||(
|
|
388
|
+
left>=0&&Math.abs(caret-anchors[left].record.end)<=Math.abs(anchors[right].record.start-caret)
|
|
389
|
+
);
|
|
390
|
+
const index=useLeft?left--:right++;
|
|
391
|
+
const anchor=anchors[index];
|
|
392
|
+
const range=document.createRange();
|
|
393
|
+
range.setStartAfter(anchor.node);
|
|
394
|
+
if(anchors[index+1]){
|
|
395
|
+
range.setEndBefore(anchors[index+1].node);
|
|
396
|
+
}else{
|
|
397
|
+
range.setEnd(preview,preview.childNodes.length);
|
|
398
|
+
}
|
|
399
|
+
const bounds=range.getBoundingClientRect();
|
|
400
|
+
if(!bounds.height){
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
const viewportTop=preview.getBoundingClientRect().top+preview.clientTop;
|
|
404
|
+
const viewportBottom=viewportTop+preview.clientHeight;
|
|
405
|
+
if(bounds.height<=preview.clientHeight){
|
|
406
|
+
if(bounds.top<viewportTop){
|
|
407
|
+
preview.scrollTop+=bounds.top-viewportTop;
|
|
408
|
+
}else if(bounds.bottom>viewportBottom){
|
|
409
|
+
preview.scrollTop+=bounds.bottom-viewportBottom;
|
|
410
|
+
}
|
|
411
|
+
}else{
|
|
412
|
+
// A top-level block may be a long paragraph, list or code fence.
|
|
413
|
+
// Follow approximate position within it without rewriting text.
|
|
414
|
+
const span=anchor.record.end-anchor.record.start;
|
|
415
|
+
const fraction=span?Math.max(0,Math.min(1,(caret-anchor.record.start)/span)):0;
|
|
416
|
+
const position=bounds.top+fraction*bounds.height;
|
|
417
|
+
if(position<viewportTop||position>viewportBottom){
|
|
418
|
+
preview.scrollTop+=position-viewportTop-preview.clientHeight/2;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
267
423
|
}
|
|
268
424
|
|
|
269
425
|
function formatMarkdown(event){
|
|
@@ -414,6 +570,7 @@
|
|
|
414
570
|
destroyed=true;
|
|
415
571
|
supersedeSave('markdown-editor-destroyed');
|
|
416
572
|
lifecycleController.abort('markdown-editor-destroyed');
|
|
573
|
+
previewFollowPending=false;
|
|
417
574
|
if(previewFrame){
|
|
418
575
|
cancelAnimationFrame(previewFrame);
|
|
419
576
|
previewFrame=0;
|
|
@@ -80,8 +80,22 @@ class FileEntity {
|
|
|
80
80
|
(this.fileName.lastIndexOf('.')+1)
|
|
81
81
|
).toLowerCase();
|
|
82
82
|
|
|
83
|
+
// Storage may omit MIME metadata; preserve a supplied type before inferring audio.
|
|
84
|
+
const audioMimeTypes={
|
|
85
|
+
"mp3": "audio/mpeg",
|
|
86
|
+
"wav": "audio/wav",
|
|
87
|
+
"ogg": "audio/ogg",
|
|
88
|
+
"oga": "audio/ogg",
|
|
89
|
+
"opus": "audio/ogg",
|
|
90
|
+
"aac": "audio/aac",
|
|
91
|
+
"m4a": "audio/mp4",
|
|
92
|
+
"flac": "audio/flac",
|
|
93
|
+
"weba": "audio/webm"
|
|
94
|
+
};
|
|
95
|
+
|
|
83
96
|
const mime=mimeTypes[file.ext]||
|
|
84
97
|
file.type||
|
|
98
|
+
audioMimeTypes[file.ext]||
|
|
85
99
|
'application/octet-stream';
|
|
86
100
|
|
|
87
101
|
file.mime=mime;
|
|
@@ -1101,6 +1101,8 @@ function normalizeMarkdownOptions(input={},previous={}){
|
|
|
1101
1101
|
showPreview:option(input,previous,'showPreview',true)!==false,
|
|
1102
1102
|
showToolbar:option(input,previous,'showToolbar',true)!==false,
|
|
1103
1103
|
showSave:option(input,previous,'showSave',true)!==false,
|
|
1104
|
+
fit:option(input,previous,'fit',false)===true,
|
|
1105
|
+
followPreview:option(input,previous,'followPreview',false)===true,
|
|
1104
1106
|
readOnly:option(input,previous,'readOnly',false)===true,
|
|
1105
1107
|
clearOnSave:option(input,previous,'clearOnSave',true)!==false,
|
|
1106
1108
|
onSave:optionalCallback(input,previous,['onSave','save'],'onSave'),
|
|
@@ -1,30 +1,36 @@
|
|
|
1
1
|
import { arcaneLogging } from 'arcane-os/logging';
|
|
2
|
-
import { marked } from './Marked.min.js';
|
|
2
|
+
import { Marked, marked } from './Marked.min.js';
|
|
3
3
|
import Is from 'strong-type';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
link(href, title, text)
|
|
12
|
-
|
|
13
|
-
return link.replace("<a","<a target='_blank' ");
|
|
14
|
-
}
|
|
5
|
+
const markdownOptions = {
|
|
6
|
+
async: false,
|
|
7
|
+
pedantic: false,
|
|
8
|
+
gfm: true,
|
|
9
|
+
renderer: {
|
|
10
|
+
link(href, title, text) {
|
|
11
|
+
const link = marked.Renderer.prototype.link.call(this, href, title, text);
|
|
12
|
+
return link.replace("<a","<a target='_blank' ");
|
|
15
13
|
}
|
|
16
14
|
}
|
|
17
|
-
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
marked.use(markdownOptions);
|
|
18
18
|
|
|
19
19
|
const is = new Is(false);
|
|
20
20
|
|
|
21
21
|
class MD {
|
|
22
22
|
#raw='';
|
|
23
|
-
#rendered=''
|
|
23
|
+
#rendered='';
|
|
24
|
+
#sourceMapEnabled=false;
|
|
25
|
+
#sourceMap=[];
|
|
24
26
|
|
|
25
|
-
constructor(raw=''){
|
|
27
|
+
constructor(raw='', options={}){
|
|
28
|
+
this.#sourceMapEnabled=options?.sourceMap===true;
|
|
26
29
|
this.raw=raw;
|
|
27
|
-
|
|
30
|
+
if(!is.string(raw)){
|
|
31
|
+
// Preserve the constructor's existing Marked error for invalid input.
|
|
32
|
+
this.rendered=marked.parse(raw);
|
|
33
|
+
}
|
|
28
34
|
return this;
|
|
29
35
|
}
|
|
30
36
|
|
|
@@ -36,6 +42,10 @@ class MD {
|
|
|
36
42
|
return this.#rendered;
|
|
37
43
|
}
|
|
38
44
|
|
|
45
|
+
get sourceMap(){
|
|
46
|
+
return this.#sourceMap;
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
set rendered(value=''){
|
|
40
50
|
return this.#rendered;
|
|
41
51
|
}
|
|
@@ -50,7 +60,7 @@ class MD {
|
|
|
50
60
|
return this.#raw;
|
|
51
61
|
}
|
|
52
62
|
this.#raw = value;
|
|
53
|
-
this.#
|
|
63
|
+
this.#render();
|
|
54
64
|
return this.#raw;
|
|
55
65
|
}
|
|
56
66
|
|
|
@@ -60,9 +70,130 @@ class MD {
|
|
|
60
70
|
return this.#raw;
|
|
61
71
|
}
|
|
62
72
|
this.#raw += value;
|
|
63
|
-
this.#
|
|
73
|
+
this.#render();
|
|
64
74
|
return this.#raw;
|
|
65
75
|
}
|
|
76
|
+
|
|
77
|
+
#render(){
|
|
78
|
+
if(!this.#sourceMapEnabled){
|
|
79
|
+
this.#rendered=marked.parse(this.#raw);
|
|
80
|
+
this.#sourceMap=[];
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const result=renderWithSourceMap(this.#raw);
|
|
85
|
+
this.#rendered=result.rendered;
|
|
86
|
+
this.#sourceMap=result.sourceMap;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function renderWithSourceMap(raw){
|
|
91
|
+
const tokenRanges=new WeakMap();
|
|
92
|
+
const sourceMap=[];
|
|
93
|
+
let normalizedEnd=0;
|
|
94
|
+
let normalizedOffset=0;
|
|
95
|
+
let originalOffset=0;
|
|
96
|
+
let observedRoot=false;
|
|
97
|
+
let previousOffset=0;
|
|
98
|
+
let previousCount=0;
|
|
99
|
+
let previousRaw;
|
|
100
|
+
const authoredGenerations=new Set();
|
|
101
|
+
for(const match of raw.matchAll(/arcane-md-source-(\d+):/g)){
|
|
102
|
+
authoredGenerations.add(match[1]);
|
|
103
|
+
}
|
|
104
|
+
let markerGeneration=0;
|
|
105
|
+
while(authoredGenerations.has(String(markerGeneration))){
|
|
106
|
+
markerGeneration++;
|
|
107
|
+
}
|
|
108
|
+
const markerPrefix=`arcane-md-source-${markerGeneration}:`;
|
|
109
|
+
|
|
110
|
+
function captureBoundary(tokens, offset){
|
|
111
|
+
// Follow Marked's CRLF/CR cursor without rewriting raw or retaining a per-character map.
|
|
112
|
+
while(normalizedOffset<offset){
|
|
113
|
+
if(raw[originalOffset]==='\r' && raw[originalOffset+1]==='\n'){
|
|
114
|
+
originalOffset++;
|
|
115
|
+
}
|
|
116
|
+
originalOffset++;
|
|
117
|
+
normalizedOffset++;
|
|
118
|
+
}
|
|
119
|
+
for(let index=previousCount; index<tokens.length; index++){
|
|
120
|
+
tokenRanges.set(
|
|
121
|
+
tokens[index],
|
|
122
|
+
{
|
|
123
|
+
start:previousOffset,
|
|
124
|
+
end:originalOffset
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const last=tokens.at(-1);
|
|
130
|
+
if(tokens.length===previousCount && last && last.raw!==previousRaw){
|
|
131
|
+
// Marked can merge a newline or indented continuation into a prior token.
|
|
132
|
+
const range=tokenRanges.get(last);
|
|
133
|
+
if(range){
|
|
134
|
+
range.end=originalOffset;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
previousOffset=originalOffset;
|
|
138
|
+
previousCount=tokens.length;
|
|
139
|
+
previousRaw=last?.raw;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const renderer={};
|
|
143
|
+
const blockTypes=['code','blockquote','html','heading','hr','list','paragraph','table','text'];
|
|
144
|
+
for(const type of blockTypes){
|
|
145
|
+
const render=marked.Renderer.prototype[type];
|
|
146
|
+
renderer[type]=function renderMappedBlock(token){
|
|
147
|
+
const html=render.call(this,token);
|
|
148
|
+
const range=tokenRanges.get(token);
|
|
149
|
+
if(!range || !html){
|
|
150
|
+
return html;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const marker=`${markerPrefix}${sourceMap.length}`;
|
|
154
|
+
sourceMap.push(
|
|
155
|
+
{
|
|
156
|
+
start:range.start,
|
|
157
|
+
end:range.end,
|
|
158
|
+
marker,
|
|
159
|
+
type:token.type
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
return `<!--${marker}-->${html}`;
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const parser=new Marked(
|
|
167
|
+
markdownOptions,
|
|
168
|
+
{
|
|
169
|
+
extensions:[
|
|
170
|
+
{
|
|
171
|
+
name:'arcaneSourceMap',
|
|
172
|
+
level:'block',
|
|
173
|
+
tokenizer(src,tokens){
|
|
174
|
+
if(tokens===this.lexer.tokens){
|
|
175
|
+
if(!observedRoot){
|
|
176
|
+
normalizedEnd=src.length;
|
|
177
|
+
observedRoot=true;
|
|
178
|
+
}
|
|
179
|
+
// Observe consumption, including definitions that produce no token.
|
|
180
|
+
captureBoundary(tokens,normalizedEnd-src.length);
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
],
|
|
186
|
+
hooks:{
|
|
187
|
+
processAllTokens(tokens){
|
|
188
|
+
captureBoundary(tokens,normalizedEnd);
|
|
189
|
+
return tokens;
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
renderer
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
const rendered=parser.parse(raw);
|
|
196
|
+
return {rendered,sourceMap};
|
|
66
197
|
}
|
|
67
198
|
|
|
68
199
|
export default MD;
|