rapier-markdown-kit 1.1.84 → 1.1.86

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/LICENSE CHANGED
@@ -49,3 +49,7 @@ re-imports and re-exports that surface unchanged (one owner, no copy) and keeps
49
49
  needs the AGPL decoders, so it is AGPL-3.0-only like the rest of the editor. The `rapier-markdown-kit`
50
50
  package re-exports `spec/md-assets.mjs` directly, never `images/assets.mjs`, so its import graph
51
51
  never touches AGPL code.
52
+
53
+ The JPEG XL encoder and worker modules in `images/jxl/` carry their MIT license in
54
+ `images/jxl/LICENSE`. The MIT-marked `images/encoder.mjs` and `images/codec.mjs`
55
+ re-export those modules. Their runtime import graph contains no editor code.
package/README.md CHANGED
@@ -30,9 +30,8 @@ import {parseAssets} from 'rapier-markdown-kit/assets'; // the picture appendi
30
30
 
31
31
  `rapier-markdown-kit/render` exports `createRenderer`, `createMarkdownRenderer`,
32
32
  `createRenderStyles`, `createRenderSanitizer` and `createPrintRenderer`. These are the
33
- actual editor/Share owners, not a second Markdown dialect or a lookalike stylesheet.
34
- Their host dependencies are explicit named properties in each factory's first argument;
35
- no factory reaches into an editor session to find its document. `style.css` is the exact
33
+ functions used by the editor and exported pages. Each factory takes explicit host dependencies
34
+ in its first argument. `style.css` is the
36
35
  MIT `spec/markdown-style.css`, including the numbered-list, checkbox, callout, table,
37
36
  footnote and picture-layout treatment.
38
37
 
@@ -52,20 +51,15 @@ Every existing lower-level method is also returned for hosts that compose the st
52
51
  Create a new bound instance when its parser or host state changes; do not share mutable
53
52
  DOM objects between document realms.
54
53
 
55
- This is a **DOM-hosted library**, not a bundled headless browser, DOM shim, parser or
56
- image decoder. Supplying only `{source}` or running it in bare Node without those ports
57
- is not supported. A host must provide a real DOM and sanitizer, the Rapier grammar,
54
+ This **DOM-hosted library** requires a DOM and sanitizer, the Rapier grammar,
58
55
  portable image preparation, style access and the named codec/layout ports it uses.
59
56
  Those dependencies keep their own licences: the MIT grant here does not relicense a
60
57
  separately supplied host or vendor. The repository's `editor/render-host.js` binds the
61
58
  editor; `server/` provides a complete local host with a pinned Chromium process. Its
62
59
  Word exporter and filesystem service are AGPL, not part of this MIT package.
63
60
 
64
- The extraction receipt in the existing `html-export-corpus` row fixes all 226 baseline
65
- writer pages byte for byte. That corpus supplies a clean DOM and named style/sanitizer
66
- adapters: it proves serialization and source recovery, not browser appearance or
67
- sanitizer security. The server's integration cells additionally use a real DOM and
68
- round-trip the actual HTTP output.
61
+ Serialization tests preserve source bytes through HTML export and recovery. Server integration
62
+ tests also check the exported HTTP response through a browser DOM.
69
63
 
70
64
  ## What it gives you
71
65
 
@@ -119,6 +119,17 @@ function initMarkdownIt() {
119
119
  if (!env.rapierPage && typeof globalThis.RapierEmbeddedImages?.imageHtml === 'function' &&
120
120
  (_rapierEmbedAssetSource?.(src) || asset && (reference || asset.codec === 'image/jxl' || asset.codec === 'image/svg+xml'))) return globalThis.RapierEmbeddedImages.imageHtml(
121
121
  reference, alt, title, size, layout, rawAlt, token.meta?.mdImage?.source, src);
122
+ let loadable = false;
123
+ try { loadable = !!src.trim() && /^(?:https?|file|blob|data):$/.test(new URL(src, globalThis.document?.baseURI || globalThis.location?.href || 'https://rapier.invalid/').protocol); } catch (_) {}
124
+ if (!loadable) {
125
+ const escape = md.utils.escapeHtml;
126
+ return '<span data-rapier-markdown-image="" data-rapier-remote-src="' + escape(src) + '" data-rapier-remote-alt="' + escape(alt) + '"'
127
+ + (title ? ' data-rapier-remote-title="' + escape(title) + '"' : '')
128
+ + (size ? ' data-rapier-image-alt-source="' + escape(rawAlt) + '"' : '')
129
+ + (layout ? ' data-rapier-image-layout="' + escape(encodeURIComponent(layout)) + '"' : '')
130
+ + (token.meta?.mdImage?.source ? ' data-rapier-image-source="' + escape(encodeURIComponent(token.meta.mdImage.source)) + '"' : '')
131
+ + '>' + escape(alt || src) + '</span>';
132
+ }
122
133
  if (!env.rapierPage && !_rapierRemoteContent.allowed && _rapierRemoteSubresourceOrigin(src)) {
123
134
  const html = _rapierRemoteImagePlaceholder(src, alt, title, size, layout, rawAlt);
124
135
  return html.replace('<span ', '<span data-rapier-markdown-image="" ' +
@@ -906,9 +917,9 @@ function _rapierProjectPortableRoot(semanticRoot, options) {
906
917
  // consumers that read HTML -- publishing/fragment export, TXT. DOCX is not an HTML consumer: its
907
918
  // writer (interchange/docx.mjs) reads the layout comment itself, so stripping it first and
908
919
  // handing back CSS only throws the fact away before the writer ever sees it. `keepLayoutData`
909
- // leaves `data-rapier-image-layout` (width, align AND rotation) and the paragraph's
910
- // `data-md-align` exactly as the semantic root already carries them, instead of the CSS-only
911
- // projection, so the one existing layout representation reaches the writer.
920
+ // leaves the image and text layout records, ink meaning and paragraph alignment exactly as
921
+ // the semantic root carries them. Ordinary paragraph/list CSS measurements also reach their
922
+ // native reader; no derived private CSS is needed.
912
923
  const keepLayoutData = !!opts.keepLayoutData;
913
924
  const root = semanticRoot.cloneNode(true), nativeElements = new Set();
914
925
  // A source soft break is a reading space, never the editor's invisible token.
@@ -986,10 +997,16 @@ function _rapierProjectPortableRoot(semanticRoot, options) {
986
997
  root.querySelectorAll('*').forEach(element => {
987
998
  if (nativeElements.has(element)) return;
988
999
  const lang = element.tagName === 'CODE' ? _rapierLanguageClass(element) : '';
1000
+ // The native note reader needs these transient roles to omit the renderer's
1001
+ // separator and return-link spacing before it compares source-owned review notes.
1002
+ const footnoteRole = keepLayoutData ? ['footnote-backref', 'footnotes-sep']
1003
+ .filter(name => element.classList.contains(name)).join(' ') : '';
989
1004
  const columnAlignment = /^(TH|TD)$/.test(element.tagName) && /^(left|center|right)$/.test(element.style.textAlign)
990
1005
  ? element.style.textAlign : '';
991
1006
  const alignment = globalThis.RapierMarkdownLayout.parseLayoutAttribute(element.getAttribute('data-md-layout'))?.align || columnAlignment;
992
1007
  const imageLayout = element.tagName === 'IMG' ? globalThis.RapierMarkdownLayout.parseLayoutAttribute(element.getAttribute('data-rapier-image-layout')) : null;
1008
+ const measurements = keepLayoutData && /^(?:P|H[1-6]|LI|OL|UL)$/.test(element.tagName)
1009
+ ? ['text-indent', 'margin-inline-start'].map(property => [property, element.style.getPropertyValue(property)]) : [];
993
1010
 
994
1011
  const colorHex = element.tagName === 'SPAN' ? element.getAttribute('data-md-color') : null;
995
1012
 
@@ -1002,14 +1019,16 @@ function _rapierProjectPortableRoot(semanticRoot, options) {
1002
1019
  const isPageBreak = element.hasAttribute('data-md-break');
1003
1020
  Array.from(element.attributes).forEach(attribute => {
1004
1021
  const name = attribute.name.toLowerCase();
1005
- if (keepLayoutData && (name === 'data-rapier-image-layout' || name === 'data-md-align')) return;
1022
+ if (keepLayoutData && (name === 'data-rapier-image-layout' || name === 'data-md-layout' || name === 'data-md-align' || name === 'data-rapier-ink')) return;
1006
1023
  if (name === 'class' || name === 'style' || name.startsWith('data-')
1007
1024
  || name === 'contenteditable' || name === 'spellcheck'
1008
1025
  || name === 'tabindex' || name === 'disabled' || name === 'aria-disabled') {
1009
1026
  element.removeAttribute(attribute.name);
1010
1027
  }
1011
1028
  });
1029
+ if (footnoteRole) element.className = footnoteRole;
1012
1030
  if (lang) element.className = 'language-' + lang;
1031
+ for (const [property, value] of measurements) if (value) element.style.setProperty(property, value);
1013
1032
  if (alignment) element.style.textAlign = alignment;
1014
1033
  if (imageLayout && !keepLayoutData) {
1015
1034
  const pictureStyle = globalThis.RapierMarkdownLayout.imageStyle(imageLayout);
@@ -1051,11 +1070,11 @@ function _rapierPictureWords(alt, src) {
1051
1070
  return alt ? '[Image: ' + alt + ']' : '[Image]';
1052
1071
  }
1053
1072
 
1054
- // A remote picture written as Markdown stands in the editor, until the person allows remote content, as a placeholder: its words, its origin and a
1055
- // button. The text file says the picture as it does once the person has allowed it, its words and its address, never the placeholder's controls.
1073
+ // Held Markdown and HTML pictures keep their addresses as inert data. Text export reads them before
1074
+ // the portable projection removes editor attributes, without restoring a fetching image source.
1056
1075
  function _rapierHeldPicturesAsWords(root) {
1057
1076
  const copy = root.cloneNode(true);
1058
- copy.querySelectorAll('span[data-rapier-remote-src]').forEach(held => held.replaceWith(document.createTextNode(
1077
+ copy.querySelectorAll('span[data-rapier-remote-src],img[data-rapier-remote-src]').forEach(held => held.replaceWith(document.createTextNode(
1059
1078
  _rapierPictureWords(String(held.getAttribute('data-rapier-remote-alt') || '').trim(), String(held.getAttribute('data-rapier-remote-src') || '').trim()))));
1060
1079
  return copy;
1061
1080
  }
@@ -1133,7 +1152,7 @@ function _rapierPortablePlainText(root, options) {
1133
1152
  const opts = options || {};
1134
1153
  const blocks = [];
1135
1154
  const blockTags = new Set([
1136
- 'ADDRESS','ARTICLE','ASIDE','BLOCKQUOTE','DETAILS','DIV','DL','FIELDSET','FIGURE','FOOTER',
1155
+ 'ADDRESS','ARTICLE','ASIDE','BLOCKQUOTE','DETAILS','DIV','DL','FIELDSET','FIGCAPTION','FIGURE','FOOTER',
1137
1156
  'FORM','H1','H2','H3','H4','H5','H6','HEADER','HR','MAIN','NAV','OL','P','PRE','SECTION','TABLE','UL'
1138
1157
  ]);
1139
1158
 
@@ -1235,13 +1254,6 @@ function _rapierPortablePlainText(root, options) {
1235
1254
  Array.from(node.children).filter(child => child !== summary).forEach(collectBlock);
1236
1255
  return;
1237
1256
  }
1238
- if (tag === 'FIGURE') {
1239
- const image = node.querySelector(':scope > img');
1240
- if (image) pushBlock(_rapierPortableInlineText(image, opts), false);
1241
- const caption = node.querySelector(':scope > figcaption');
1242
- if (caption) pushBlock(_rapierNormalizeInlineText(_rapierPortableInlineText(caption, opts)), false);
1243
- return;
1244
- }
1245
1257
  collectChildren(node);
1246
1258
  };
1247
1259
 
@@ -270,7 +270,7 @@ function _rapierInstallSanitizeHooks() {
270
270
  && _rapierSafeRasterDataUrl(compact);
271
271
  if (!rasterAllowed) data.keepAttr = false;
272
272
  });
273
- // Paste and render sanitise: a remote <img src> would fetch the instant its markup lands in the page,
273
+ // Paste, render and export sanitise: a remote <img src> would fetch the instant its markup lands in the page,
274
274
  // so the src the hook below is about to strip never survives. Move it to a data-* attribute first
275
275
  // (DOMPurify already leaves data-* alone, and the sanitizer's own inert document never fetches it
276
276
  // either), so the picture and its alt text still reach Turndown's rapierImage rule as a live
@@ -279,7 +279,7 @@ function _rapierInstallSanitizeHooks() {
279
279
  // only the three this hook sets on the node it holds back stay. A picture inside a link is the same
280
280
  // node with the same fix: the outer <a> rule reads this img's own converted markdown as its content.
281
281
  DOMPurify.addHook('uponSanitizeElement', node => {
282
- if (_rapierRemoteContent.allowed || !/^(?:paste|render|raw)$/.test(_rapierSanitizeRuntime.context)) return;
282
+ if (_rapierRemoteContent.allowed || !/^(?:paste|render|raw|export)$/.test(_rapierSanitizeRuntime.context)) return;
283
283
  if (!node || node.nodeName !== 'IMG' || node.hasAttribute('data-rapier-remote-src')) return;
284
284
  const src = node.getAttribute('src') || '';
285
285
  if (!src || !_rapierRemoteSubresourceOrigin(src)) return;
@@ -318,6 +318,8 @@ function _rapierInstallSanitizeHooks() {
318
318
  return;
319
319
  }
320
320
  const attribute = String(data.attrName || '').toLowerCase();
321
+ // These attributes carry authored text, never resource requests.
322
+ if (attribute === 'alt' || attribute === 'title') return;
321
323
  if (/^data-rapier-remote-(?:src|alt|title)$/.test(attribute) && node && node._rapierHeldBack === true) { data.forceKeepAttr = true; return; }
322
324
  if (attribute.startsWith('data-')) return;
323
325
  const element = String(node && node.nodeName || '').toUpperCase();
@@ -4,17 +4,25 @@
4
4
  // `opacity`: a picture's fade, a whole percent from 5 to 100, omitted at 100; the picture's bytes never change.
5
5
  // `lines`: a picture's height in lines of the text beside it, set as a drop cap is (CSS `initial-letter`): its top on the first line's cap height, its foot on the
6
6
  // Nth line's baseline, its width from its own aspect. A whole number 1 to 12; never with `width`.
7
- // `first`/`indent`: a paragraph's first-line indent and its whole-block indent, in levels of one step (2em), a whole number from 1 to 4, omitted when 0; text
8
- // only.
7
+ // `first`/`indent`: signed levels of 2em, at most six decimals, in [-64,64], omitted at zero. `marker` moves a list
8
+ // item's marker independently of its words, on the same scale; only the item's leading paragraph carries it.
9
9
  // `lock=on`: a picture locked in place: a tap goes to the words, never the picture; a hold takes it to unlock it. Omitted when off.
10
- export const fields = new Set(['align', 'width', 'lines', 'wrap', 'x', 'y', 'rotate', 'opacity', 'first', 'indent', 'lock']);
10
+ export const fields = new Set(['align', 'width', 'lines', 'wrap', 'x', 'y', 'rotate', 'opacity', 'first', 'indent', 'marker', 'lock']);
11
11
  export const alignments = new Set(['left', 'center', 'right', 'justify']);
12
12
  // `behind`/`front`: out of flow; the paragraph lays out as though the picture were absent.
13
13
  export const wraps = new Set(['around', 'box', 'behind', 'front']);
14
14
  const has = (value, key) => Object.hasOwn(value, key);
15
15
  const percent = value => typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 100;
16
16
  const lineCount = value => Number.isInteger(value) && value >= 1 && value <= 12;
17
- const level = value => Number.isInteger(value) && value >= 1 && value <= 4;
17
+ export const INDENT_LIMIT = 64;
18
+ export const indentLevel = value => typeof value === 'number' && Number.isFinite(value) && value !== 0 &&
19
+ Math.abs(value) <= INDENT_LIMIT && Number(value.toFixed(6)) === value;
20
+ // A key press advances an imported measure by one level without truncating its fraction or jumping across zero.
21
+ export function stepIndent(value, step) {
22
+ const next = Number(((value || 0) + step).toFixed(6));
23
+ if (Math.abs(next) > INDENT_LIMIT) return value || 0;
24
+ return !value && step < 0 || value && Math.sign(value) !== Math.sign(next) ? 0 : next;
25
+ }
18
26
  const oneDecimalDegrees = value => typeof value === 'number' && Number.isFinite(value) &&
19
27
  value > -180 && value <= 180 && Math.abs(value * 10 - Math.round(value * 10)) < 1e-9;
20
28
 
@@ -30,7 +38,8 @@ export function validLayout(value) {
30
38
  (!has(value, 'y') || typeof value.y === 'number' && Number.isFinite(value.y) && value.y >= -50 && has(value, 'wrap')) &&
31
39
  (!has(value, 'rotate') || oneDecimalDegrees(value.rotate)) &&
32
40
  (!has(value, 'opacity') || Number.isInteger(value.opacity) && value.opacity >= 5 && value.opacity <= 100) &&
33
- (!has(value, 'first') || level(value.first)) && (!has(value, 'indent') || level(value.indent)) &&
41
+ (!has(value, 'first') || indentLevel(value.first)) && (!has(value, 'indent') || indentLevel(value.indent)) &&
42
+ (!has(value, 'marker') || indentLevel(value.marker)) &&
34
43
  (!has(value, 'lock') || value.lock === 'on') &&
35
44
  !(has(value, 'align') && (has(value, 'wrap') || has(value, 'x')));
36
45
  }
@@ -69,8 +78,8 @@ export function parseLayout(comment) {
69
78
  } else if (key === 'lines') {
70
79
  if (!/^[1-9]\d?$/.test(raw)) return null;
71
80
  value.lines = Number(raw);
72
- } else if (key === 'first' || key === 'indent') {
73
- if (!/^[1-4]$/.test(raw)) return null;
81
+ } else if (key === 'first' || key === 'indent' || key === 'marker') {
82
+ if (!/^-?(?:0|[1-9]\d?)(?:\.\d{1,6})?$/.test(raw)) return null;
74
83
  value[key] = Number(raw);
75
84
  } else value[key] = raw;
76
85
  }
@@ -85,6 +94,38 @@ export function decodeLayoutAttribute(value) {
85
94
 
86
95
  export const parseLayoutAttribute = value => parseLayout(decodeLayoutAttribute(value));
87
96
 
97
+ // Indentation uses the document's body type, including on headings. Native table measurements
98
+ // become ordinary CSS points; an item's margin is compensated by its paragraph and nested list.
99
+ export function textStyle(value, {portable = false, pointSize, indentOffset = 0} = {}) {
100
+ if (!validLayout(value)) return '';
101
+ const property = {first: 'text-indent', indent: 'margin-inline-start'};
102
+ return (portable ? ['first', 'indent'] : ['first', 'indent', 'marker'])
103
+ .filter(key => has(value, key) || key === 'indent' && indentOffset).map(key => {
104
+ const level = (value[key] || 0) + (key === 'indent' ? indentOffset : 0);
105
+ const measure = decimal(Number((level * 2).toFixed(6)));
106
+ return (portable ? property[key] : '--md-' + key) + ':' + (portable ?
107
+ pointSize ? decimal(Math.round(level * pointSize * 40) / 20) + 'pt' : measure + 'em' :
108
+ 'calc(' + measure + ' * var(--md-text-body))');
109
+ }).join(';');
110
+ }
111
+
112
+ // Raw HTML tables carry ordinary paragraph CSS. Read the browser's parsed declarations on the same
113
+ // admitted scale; the native caller supplies the body's point size. A list marker belongs to its own li,
114
+ // never to a paragraph or a picture.
115
+ export function textLayout(style, {listMarker = false, pointSize, offset = 0} = {}) {
116
+ const value = {};
117
+ for (const [key, property] of (listMarker ? [['marker', 'margin-inline-start']] : [['first', 'text-indent'], ['indent', 'margin-inline-start']])) {
118
+ const raw = style?.getPropertyValue(property).trim() || '';
119
+ const match = /^(-?(?:0|[1-9]\d*)(?:\.\d{1,6})?)(em|pt)$/.exec(raw);
120
+ const level = Number(((match ? Number(match[1]) / (match[2] === 'pt' ? pointSize : 1) / 2 : 0) +
121
+ (key === 'first' ? 0 : offset)).toFixed(6));
122
+ // Public CSS may compensate an enclosing item's margin; the resolved native coordinate,
123
+ // rather than each intermediate declaration, is the measurement the writer must keep.
124
+ if (Number.isFinite(level) && level) value[key] = level;
125
+ }
126
+ return value;
127
+ }
128
+
88
129
  // The picture's own CSS: its normal-flow width and x, its height in lines, and its fade. `1cap` is the cap height of the
89
130
  // picture's own text; a renderer that sets the picture beside its anchor measures that paragraph's line and cap instead.
90
131
  export function imageStyle(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rapier-markdown-kit",
3
- "version": "1.1.84",
3
+ "version": "1.1.86",
4
4
  "description": "Read, render, write and style Self-contained Markdown, and carry its optional edit ledger: the shared HTML writer, semantic renderer, stylesheet, layout, marks, pictures, Will/1, replay, authorship and offline merge. MIT, no npm dependencies.",
5
5
  "keywords": [
6
6
  "markdown",
package/style.css CHANGED
@@ -132,14 +132,8 @@
132
132
  :where(.md-render) [data-md-align="center"]{text-align:center!important}
133
133
  :where(.md-render) [data-md-align="right"]{text-align:right!important}
134
134
  :where(.md-render) [data-md-align="justify"]{text-align:justify!important}
135
- :where(.md-render) p[data-md-first="1"]{text-indent:2em}
136
- :where(.md-render) p[data-md-first="2"]{text-indent:4em}
137
- :where(.md-render) p[data-md-first="3"]{text-indent:6em}
138
- :where(.md-render) p[data-md-first="4"]{text-indent:8em}
139
- :where(.md-render) p[data-md-indent="1"]{margin-inline-start:2em}
140
- :where(.md-render) p[data-md-indent="2"]{margin-inline-start:4em}
141
- :where(.md-render) p[data-md-indent="3"]{margin-inline-start:6em}
142
- :where(.md-render) p[data-md-indent="4"]{margin-inline-start:8em}
135
+ :where(.md-render) p[data-md-first]{--md-first:0px;text-indent:var(--md-first)}
136
+ .md-render :is(p,h1,h2,h3,h4,h5,h6)[data-md-indent]{--md-indent:0px;margin-inline-start:var(--md-indent)}
143
137
  .md-render p[data-md-align="center"]>img{margin-inline:auto}
144
138
  .md-render p[data-md-align="right"]>img{margin-left:auto;margin-right:0}
145
139
  :where(.md-render) p[data-md-layout-tight]{margin-block:0!important}
@@ -264,10 +258,10 @@
264
258
  .md-render p{margin:0 0 var(--md-line)}
265
259
  .md-render ul,.md-render ol{margin:0 0 var(--md-line);padding-inline-start:var(--md-space-5)}
266
260
  .md-render ul{list-style:none}
267
- .md-render ul>li{position:relative}
261
+ .md-render ul>li{position:relative;--md-marker:0px}
268
262
 
269
263
  .md-render ul>li::before{
270
- content:'';position:absolute;inset-inline-start:calc(-1*var(--md-space-4));top:.66em;
264
+ content:'';position:absolute;inset-inline-start:calc(-1*var(--md-space-4) + var(--md-marker, 0px));top:.66em;
271
265
  width:7px;height:7px;border-radius:50%;background:currentColor}
272
266
  .md-render ul ul>li::before{
273
267
  box-sizing:border-box;background:transparent;border:2px solid currentColor}
@@ -347,6 +341,7 @@
347
341
  }
348
342
  .md-render ol>li{
349
343
  position:relative;
344
+ --md-marker:0px;
350
345
  counter-increment:rapier-ol;
351
346
  min-height:calc(2 * var(--md-unit));
352
347
  padding-top:calc(0.15 * var(--md-unit));
@@ -355,7 +350,7 @@
355
350
  content:counter(rapier-ol);
356
351
  position:absolute;
357
352
  /* The circle is centred on the item's first line: the li's own padding, plus half of what the 2rem circle is taller than one line. */
358
- inset-inline-start:calc(-2.75 * var(--md-unit)); top:calc(calc(0.15 * var(--md-unit)) + (var(--md-line) - calc(2 * var(--md-unit))) / 2);
353
+ inset-inline-start:calc(-2.75 * var(--md-unit) + var(--md-marker, 0px)); top:calc(calc(0.15 * var(--md-unit)) + (var(--md-line) - calc(2 * var(--md-unit))) / 2);
359
354
  width:calc(2 * var(--md-unit)); height:calc(2 * var(--md-unit));
360
355
  display:flex; align-items:center; justify-content:center;
361
356
  border:2px solid var(--md-color-line);
@@ -372,7 +367,7 @@
372
367
  content:'';
373
368
  position:absolute;
374
369
 
375
- inset-inline-start:calc(calc(-1.75 * var(--md-unit)) - 1.5px);
370
+ inset-inline-start:calc(calc(-1.75 * var(--md-unit)) - 1.5px + var(--md-marker, 0px));
376
371
  top:calc(2.2 * var(--md-unit));
377
372
  bottom:calc(var(--md-line) / -4);
378
373
  border-inline-start:3px dotted var(--md-color-line);