weml-monaco 0.10.1 → 0.11.1

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 CHANGED
@@ -8,6 +8,31 @@ This package shares the language logic of the VS Code extension (`weml-support`)
8
8
  entries below track the port catching up with its provider changes; since the shared language
9
9
  features, both take the same code.
10
10
 
11
+ ## [0.11.1] — 2026-10-15
12
+
13
+ ### Changed
14
+ - The README is trimmed to what a user of the package needs.
15
+
16
+ ## [0.11.0] — 2026-10-14
17
+
18
+ ### Added
19
+ - `fixedWrappers` (paragraph mode, `createWemlEditor` and `<WemlEditor>`): fixed tags between the
20
+ `<div id="1">` and the content, outermost first — a name or `{ tag, attributes }`. `['w-para']`
21
+ edits the content of a `<w-para>`, as a publication's description is: `<div id="1"><w-para>` …
22
+ `</w-para></div>`, both lines locked as the `<div>` was, `getValue()` / `onChange` the content
23
+ inside them, the preview rendering it inside them (without the `<div>`). A chain WEML's structure
24
+ rules reject throws when the editor is created; with any, an empty value starts empty (the
25
+ templates are a document and a `<w-para>`).
26
+
27
+ ### Changed
28
+ - The options and the props are split by `mode` (`WemlEditorFileModeOptions` /
29
+ `WemlEditorParagraphModeOptions`, `WemlEditorFileModeProps` / `WemlEditorParagraphModeProps`):
30
+ `hideWrapper` and `fixedWrappers` type-check only with `mode: 'paragraph'`, `onFileLoad` and
31
+ `onFileSave` only in a file. A host that wraps the props in `Omit` needs a distributive one, or
32
+ the split is lost. No change at runtime.
33
+
34
+ VS Code has no counterpart: it edits files only; the paragraph mode is the web editor's.
35
+
11
36
  ## [0.10.1] — 2026-10-14
12
37
 
13
38
  ### Changed
package/README.md CHANGED
@@ -26,7 +26,7 @@ This package does **not** modify the original VS Code extension — it is a
26
26
  standalone re-implementation living entirely in this package (`packages/weml-monaco` of the
27
27
  monorepo).
28
28
 
29
- ## Install & build
29
+ ## Install
30
30
 
31
31
  ```bash
32
32
  npm install weml-monaco
@@ -44,17 +44,6 @@ The package has no runtime dependencies (`node-html-parser`,
44
44
  | `weml-monaco/editor` | `dist/editor.mjs` | the editor and its workbench (below) |
45
45
  | `weml-monaco/react` | `dist/react.mjs` | the editor as a React component |
46
46
 
47
- Working on the package itself:
48
-
49
- ```bash
50
- npm ci # at the repository root (npm workspaces)
51
- cd packages/weml-monaco
52
- npm run build # tsup → dist/ (ESM + CJS + IIFE + declarations)
53
- npm run typecheck # tsc --noEmit
54
- npm test # build + Node smoke test
55
- npm run check:package # publint + arethetypeswrong on the packed tarball
56
- ```
57
-
58
47
  ## Usage
59
48
 
60
49
  Monaco is **passed in** at registration time, so this works no matter how
@@ -187,7 +176,15 @@ weml.dispose();
187
176
  - `mode: 'paragraph'` edits one paragraph (the content of one `<div>`): the model
188
177
  holds it inside a `<div id="1">` wrapper the language needs, guarded against
189
178
  edits (dimmed, or hidden with `hideWrapper`); `getValue()` is the paragraph alone,
190
- and there is no Contents.
179
+ and there is no Contents. `fixedWrappers` puts fixed tags between the `<div>` and
180
+ the content, outermost first — `['w-para']` edits the content of a `<w-para>` (a
181
+ publication's description): the model is `<div id="1"><w-para>` … `</w-para></div>`,
182
+ both lines locked, `getValue()` the content inside them, the preview renders it
183
+ inside them. A tag is a name or `{ tag, attributes }`; one WEML does not allow
184
+ inside the one before it throws, and with any an empty value starts empty.
185
+ - The options and the component's props are split by `mode`: a paragraph's
186
+ (`hideWrapper`, `fixedWrappers`) do not type-check in a file, a file's (`onFileLoad`,
187
+ `onFileSave`) not in a paragraph.
191
188
  - **Files on disk** (`file` mode): Open File (Ctrl+O, or a `.weml` dropped on the
192
189
  editor) and Save to Disk (Ctrl+Shift+S: the name asked for at the top, suggested
193
190
  `<name>-<timestamp>.weml`; Chromium's save dialog where there is one) in the
@@ -329,7 +326,7 @@ const panel = createWemlPanel(
329
326
  ## The WEML preview
330
327
 
331
328
  A live preview of the editor's document, rendered with
332
- [`weml-stylesheet`](../weml-stylesheet) — an **optional peer dependency**:
329
+ [`weml-stylesheet`](https://github.com/Burjuy/weml-language-vscode-extension/tree/main/packages/weml-stylesheet) — an **optional peer dependency**:
333
330
  install it (and load its CSS) to use this entry; nothing else needs it.
334
331
 
335
332
  ```ts
@@ -426,25 +423,3 @@ in VS Code.
426
423
  The earlier flat DOM toolbar (`createWemlToolbar(monaco, editor, { container,
427
424
  onStatus })`) still works and still registers the editor actions, but it is
428
425
  deprecated in favour of the WEML panel and gets no new features.
429
-
430
- ## Trying it out
431
-
432
- The monorepo's dev app, [`storybook/`](../../storybook) (`weml-storybook`)
433
- (`npm start -w weml-storybook` from the repository root), runs this package from
434
- its sources in a full editor: completions (Ctrl+Space), hover, "Format
435
- Document", the outline, diagnostics and the toolbar.
436
-
437
- ## How it relates to the VS Code extension
438
-
439
- The language logic is shared with the VS Code extensions, not copied — two workspace packages,
440
- bundled into every build (neither is a runtime dependency):
441
-
442
- | shared (workspace package) | adapted for Monaco |
443
- | ------------------------------------ | ---------------------------------------- |
444
- | `weml-language-core`: schema, structure rules, snippets, entities, markdown text, the language features (diagnostics, hover, completion, outline), tool model, wrap / renumber | `schema/docs.ts` (plain `IMarkdownString`) |
445
- | `weml-normalizer`: the canonical normalizer | `wemlDocument.ts` (parse cache, offsets ↔ Monaco positions), `providers/*` (the features' data → Monaco provider types), `index.ts` (registration + diagnostics) |
446
-
447
- The diagnostics, tooltips, suggestions and outline are computed once, in the shared package, as
448
- plain data with offsets — the VS Code extension and this package only map them to their editor's
449
- types, so both editors report the same problems and offer the same items. Parsing uses
450
- `vscode-html-languageservice` (editor-agnostic, runs in the browser and in the worker).
package/dist/editor.cjs CHANGED
@@ -37666,16 +37666,36 @@ async function saveWemlFile(text, suggestedName) {
37666
37666
  }
37667
37667
 
37668
37668
  // src/editor/content.ts
37669
- var WRAPPER_OPENER = '<div id="1">';
37670
- var WRAPPER_CLOSER = "</div>";
37671
- function initialModelText(mode, content) {
37672
- return mode === "file" ? content : `${WRAPPER_OPENER}
37669
+ var DIV_OPENER = '<div id="1">';
37670
+ var DIV_CLOSER = "</div>";
37671
+ var DEFAULT_WRAPPER = { opener: DIV_OPENER, closer: DIV_CLOSER, innerOpener: "", innerCloser: "" };
37672
+ var escapeAttribute2 = (value) => value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
37673
+ function contentWrapper(fixedWrappers = []) {
37674
+ if (fixedWrappers.length === 0) return DEFAULT_WRAPPER;
37675
+ const tags = fixedWrappers.map((wrapper) => typeof wrapper === "string" ? { tag: wrapper } : wrapper);
37676
+ let parent = "div";
37677
+ for (const { tag } of tags) {
37678
+ const rule = STRUCTURE_RULES[parent];
37679
+ if (!rule?.children.some((group) => group.tags.includes(tag))) {
37680
+ throw new Error(`weml-monaco: fixedWrappers \u2014 <${tag}> is not allowed inside <${parent}>.`);
37681
+ }
37682
+ parent = tag;
37683
+ }
37684
+ const innerOpener = tags.map(({ tag, attributes }) => {
37685
+ const attrs = Object.entries(attributes ?? {}).map(([name, value]) => ` ${name}="${escapeAttribute2(value)}"`);
37686
+ return `<${tag}${attrs.join("")}>`;
37687
+ }).join("");
37688
+ const innerCloser = tags.map(({ tag }) => `</${tag}>`).reverse().join("");
37689
+ return { opener: DIV_OPENER + innerOpener, closer: innerCloser + DIV_CLOSER, innerOpener, innerCloser };
37690
+ }
37691
+ function initialModelText(mode, content, wrapper = DEFAULT_WRAPPER) {
37692
+ return mode === "file" ? content : `${wrapper.opener}
37673
37693
  ${content}
37674
- ${WRAPPER_CLOSER}`;
37694
+ ${wrapper.closer}`;
37675
37695
  }
37676
- function isWrapperIntact(model) {
37696
+ function isWrapperIntact(model, wrapper = DEFAULT_WRAPPER) {
37677
37697
  const lineCount = model.getLineCount();
37678
- return lineCount >= 3 && model.getLineContent(1) === WRAPPER_OPENER && model.getLineContent(lineCount) === WRAPPER_CLOSER;
37698
+ return lineCount >= 3 && model.getLineContent(1) === wrapper.opener && model.getLineContent(lineCount) === wrapper.closer;
37679
37699
  }
37680
37700
  function paragraphRange(model) {
37681
37701
  const lastLine = model.getLineCount() - 1;
@@ -37686,9 +37706,9 @@ function contentRange(model, mode) {
37686
37706
  const lastLine = model.getLineCount();
37687
37707
  return { startLineNumber: 1, startColumn: 1, endLineNumber: lastLine, endColumn: model.getLineMaxColumn(lastLine) };
37688
37708
  }
37689
- function readContent(model, mode) {
37709
+ function readContent(model, mode, wrapper = DEFAULT_WRAPPER) {
37690
37710
  if (mode === "file") return model.getValue();
37691
- return isWrapperIntact(model) ? model.getValueInRange(paragraphRange(model)) : null;
37711
+ return isWrapperIntact(model, wrapper) ? model.getValueInRange(paragraphRange(model)) : null;
37692
37712
  }
37693
37713
  function contentLineOffset(mode, hideWrapper) {
37694
37714
  return mode === "paragraph" && hideWrapper ? 1 : 0;
@@ -37726,17 +37746,17 @@ function findMatches(value, query, options) {
37726
37746
  if (query === "") return [];
37727
37747
  return [...value.matchAll(toRegExp(query, options))].filter((match) => match[0] !== "");
37728
37748
  }
37729
- function createContentApi(editor, mode, lineOffset, initialValue) {
37749
+ function createContentApi(editor, mode, lineOffset, initialValue, wrapper = DEFAULT_WRAPPER) {
37730
37750
  let lastWhole = initialValue;
37731
37751
  const read = () => {
37732
37752
  const model = editor.getModel();
37733
- const content = model ? readContent(model, mode) : null;
37753
+ const content = model ? readContent(model, mode, wrapper) : null;
37734
37754
  if (content !== null) lastWhole = content;
37735
37755
  return lastWhole;
37736
37756
  };
37737
37757
  const bounds = () => {
37738
37758
  const model = editor.getModel();
37739
- const content = model && readContent(model, mode);
37759
+ const content = model && readContent(model, mode, wrapper);
37740
37760
  if (!model || content === null) return void 0;
37741
37761
  const { startLineNumber, startColumn } = contentRange(model, mode);
37742
37762
  return { model, start: model.getOffsetAt({ lineNumber: startLineNumber, column: startColumn }), length: content.length };
@@ -38236,13 +38256,16 @@ function injectEditorStyles(doc = document) {
38236
38256
  // src/editor/wrapperGuard.ts
38237
38257
  var HIDDEN_AREAS_SOURCE = "weml-editor.wrapper";
38238
38258
  var WRAPPER_LINE_CLASS = "weml-editor-wrapper-line";
38239
- var WRAPPER_MESSAGE = { value: "The `<div id>` wrapper is not part of the paragraph and cannot be edited." };
38240
- function guardWrapper(m2, editor, hideWrapper, hostOptions) {
38259
+ var wrapperMessage = (wrapper) => ({
38260
+ value: `The \`${wrapper.opener}\` wrapper is not part of the content and cannot be edited.`
38261
+ });
38262
+ function guardWrapper(m2, editor, hideWrapper, hostOptions, wrapper = DEFAULT_WRAPPER) {
38241
38263
  const model = editor.getModel();
38242
38264
  if (!model) return { dispose() {
38243
38265
  }, setHostOptions() {
38244
38266
  } };
38245
38267
  let host = hostOptions;
38268
+ const message = wrapperMessage(wrapper);
38246
38269
  const disposables = [];
38247
38270
  const touchesWrapper = (selection) => selection.startLineNumber < 2 || selection.endLineNumber > model.getLineCount() - 1;
38248
38271
  const clampSelections = () => {
@@ -38263,7 +38286,7 @@ function guardWrapper(m2, editor, hideWrapper, hostOptions) {
38263
38286
  const syncReadOnly = () => {
38264
38287
  const locked = (editor.getSelections() ?? []).some(touchesWrapper);
38265
38288
  const readOnly = host.readOnly === true || locked;
38266
- const readOnlyMessage = locked && host.readOnly !== true ? WRAPPER_MESSAGE : host.readOnlyMessage;
38289
+ const readOnlyMessage = locked && host.readOnly !== true ? message : host.readOnlyMessage;
38267
38290
  if (applied?.readOnly === readOnly && applied.readOnlyMessage === readOnlyMessage) return;
38268
38291
  applied = { readOnly, readOnlyMessage };
38269
38292
  editor.updateOptions({ readOnly, readOnlyMessage });
@@ -38321,7 +38344,7 @@ function guardWrapper(m2, editor, hideWrapper, hostOptions) {
38321
38344
  let repairPending = false;
38322
38345
  disposables.push(
38323
38346
  editor.onDidChangeModelContent(() => {
38324
- if (isWrapperIntact(model)) {
38347
+ if (isWrapperIntact(model, wrapper)) {
38325
38348
  showWrapper();
38326
38349
  return;
38327
38350
  }
@@ -38329,7 +38352,7 @@ function guardWrapper(m2, editor, hideWrapper, hostOptions) {
38329
38352
  repairPending = true;
38330
38353
  queueMicrotask(() => {
38331
38354
  repairPending = false;
38332
- if (!model.isDisposed() && !isWrapperIntact(model)) model.undo();
38355
+ if (!model.isDisposed() && !isWrapperIntact(model, wrapper)) model.undo();
38333
38356
  });
38334
38357
  })
38335
38358
  );
@@ -38440,8 +38463,9 @@ var actionButton = (icon2, title, run) => {
38440
38463
  return node;
38441
38464
  };
38442
38465
  function createWemlEditor(m2, container, options = {}) {
38443
- if (options.injectStyles !== false) injectEditorStyles();
38444
38466
  const mode = options.mode ?? "file";
38467
+ const wrapper = options.mode === "paragraph" ? contentWrapper(options.fixedWrappers) : DEFAULT_WRAPPER;
38468
+ if (options.injectStyles !== false) injectEditorStyles();
38445
38469
  const parts = {
38446
38470
  tools: options.parts?.tools ?? true,
38447
38471
  activityBar: options.parts?.activityBar ?? true,
@@ -38479,20 +38503,20 @@ function createWemlEditor(m2, container, options = {}) {
38479
38503
  const language = acquireLanguage(m2, options);
38480
38504
  const { analysis, formatter } = language.registration;
38481
38505
  const given = options.value ?? "";
38482
- const initialValue = given.trim() || options.withDefaultContent === false ? given : mode === "file" ? NEW_WEML_DOCUMENT : NEW_WEML_PARAGRAPH;
38506
+ const initialValue = given.trim() || options.withDefaultContent === false || wrapper !== DEFAULT_WRAPPER ? given : mode === "file" ? NEW_WEML_DOCUMENT : NEW_WEML_PARAGRAPH;
38483
38507
  const editorId = ++editorCount;
38484
- const model = m2.editor.createModel(initialModelText(mode, initialValue), "weml", m2.Uri.parse(`inmemory://weml-editor/${editorId}/${encodeURIComponent(fileName)}`));
38508
+ const model = m2.editor.createModel(initialModelText(mode, initialValue, wrapper), "weml", m2.Uri.parse(`inmemory://weml-editor/${editorId}/${encodeURIComponent(fileName)}`));
38485
38509
  const root = element("div", { className: `${EDITOR_CLASS} monaco-component` });
38486
38510
  const editorHost = element("div", { className: "wb-editor wb-part" });
38487
38511
  const baseOptions = { ...EDITOR_OPTIONS, ...options.editorOptions };
38488
38512
  const editor = m2.editor.create(editorHost, { ...baseOptions, ...layout.editorOptions, model });
38489
- const wrapperGuard = mode === "paragraph" ? guardWrapper(m2, editor, options.hideWrapper ?? false, { ...baseOptions, ...layout.editorOptions }) : void 0;
38513
+ const wrapperGuard = mode === "paragraph" ? guardWrapper(m2, editor, options.hideWrapper ?? false, { ...baseOptions, ...layout.editorOptions }, wrapper) : void 0;
38490
38514
  if (wrapperGuard) disposables.push(wrapperGuard);
38491
38515
  const applyEditorOptions = (next) => {
38492
38516
  editor.updateOptions(next);
38493
38517
  wrapperGuard?.setHostOptions(next);
38494
38518
  };
38495
- const content = createContentApi(editor, mode, lineOffset, initialValue);
38519
+ const content = createContentApi(editor, mode, lineOffset, initialValue, wrapper);
38496
38520
  const valueListeners = /* @__PURE__ */ new Set();
38497
38521
  const problemListeners = /* @__PURE__ */ new Set();
38498
38522
  const cursorListeners = /* @__PURE__ */ new Set();
@@ -38501,7 +38525,7 @@ function createWemlEditor(m2, container, options = {}) {
38501
38525
  let problems = [];
38502
38526
  disposables.push(
38503
38527
  model.onDidChangeContent(() => {
38504
- const value = readContent(model, mode);
38528
+ const value = readContent(model, mode, wrapper);
38505
38529
  if (value === null || value === lastValue) return;
38506
38530
  lastValue = value;
38507
38531
  for (const listener of valueListeners) listener(value);
@@ -38800,11 +38824,12 @@ function createWemlEditor(m2, container, options = {}) {
38800
38824
  language: previewLanguage,
38801
38825
  scrollSync: previewScrollSync,
38802
38826
  container: host,
38803
- // A paragraph without its wrapper, from the model's second line (the scroll sync matches
38804
- // the blocks to the model's lines); a broken wrapper: the whole text, as typed.
38827
+ // A paragraph without its `<div>`, from the model's second line (the scroll sync matches
38828
+ // the blocks to the model's lines), inside its `fixedWrappers` so it is styled as their
38829
+ // content; a broken wrapper: the whole text, as typed.
38805
38830
  transform: mode === "paragraph" ? (text) => {
38806
- const paragraph = readContent(model, mode);
38807
- return paragraph === null ? text : { markup: paragraph, firstLine: 1 };
38831
+ const paragraph = readContent(model, mode, wrapper);
38832
+ return paragraph === null ? text : { markup: wrapper.innerOpener + paragraph + wrapper.innerCloser, firstLine: 1 };
38808
38833
  } : void 0
38809
38834
  });
38810
38835
  return { element: host, dispose: () => preview.dispose(), preview };
@@ -39421,7 +39446,7 @@ function createWemlEditor(m2, container, options = {}) {
39421
39446
  reset(value) {
39422
39447
  baseline = value;
39423
39448
  lastValue = value;
39424
- model.setValue(initialModelText(mode, value));
39449
+ model.setValue(initialModelText(mode, value, wrapper));
39425
39450
  renderTab();
39426
39451
  renderSave();
39427
39452
  },