mrmd-editor 0.5.0 → 0.7.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.
@@ -64,6 +64,9 @@ import {
64
64
  extractDisplayMath,
65
65
  generateMathId,
66
66
  } from './widgets/math.js';
67
+ import {
68
+ FrontmatterWidget,
69
+ } from './widgets/frontmatter.js';
67
70
 
68
71
  // =============================================================================
69
72
  // Height Cache for Stable Layout
@@ -370,6 +373,64 @@ function findDisplayMathRanges(state) {
370
373
  return ranges;
371
374
  }
372
375
 
376
+ /**
377
+ * FrontmatterWidget wrapper that caches its rendered height for stable layout.
378
+ */
379
+ class FrontmatterWidgetWithHeightCache extends FrontmatterWidget {
380
+ constructor(yamlContent, contentHash, sourceFrom, sourceTo) {
381
+ super(yamlContent, contentHash, sourceFrom, sourceTo);
382
+ }
383
+
384
+ toDOM(view) {
385
+ const dom = super.toDOM(view);
386
+ const contentHash = this.contentHash;
387
+
388
+ requestAnimationFrame(() => {
389
+ const line = dom.closest('.cm-line');
390
+ const height = line ? line.offsetHeight : dom.offsetHeight;
391
+ if (height > 0) {
392
+ cacheWidgetHeight(contentHash, height);
393
+ }
394
+ });
395
+
396
+ return dom;
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Find frontmatter range at the start of the document (--- ... ---)
402
+ */
403
+ function findFrontmatterRange(state) {
404
+ const doc = state.doc;
405
+ if (doc.lines < 2) return null;
406
+
407
+ const firstLine = doc.line(1);
408
+ if (firstLine.text.trim() !== '---') return null;
409
+
410
+ // Find closing ---
411
+ for (let i = 2; i <= doc.lines; i++) {
412
+ const line = doc.line(i);
413
+ if (line.text.trim() === '---') {
414
+ // YAML content is between line 2 and line i-1
415
+ const yamlLines = [];
416
+ for (let j = 2; j < i; j++) {
417
+ yamlLines.push(doc.line(j).text);
418
+ }
419
+ return {
420
+ type: 'frontmatter',
421
+ from: firstLine.from,
422
+ to: line.to,
423
+ startLine: 1,
424
+ endLine: i,
425
+ content: yamlLines.join('\n'),
426
+ };
427
+ }
428
+ // If we hit a line that looks like content (not YAML), stop
429
+ // Frontmatter can't contain blank lines followed by markdown
430
+ }
431
+ return null;
432
+ }
433
+
373
434
  /**
374
435
  * Build decorations for all block elements
375
436
  */
@@ -471,6 +532,48 @@ function buildBlockDecorations(state) {
471
532
  }
472
533
  }
473
534
 
535
+ // Find and process frontmatter
536
+ const fmRange = findFrontmatterRange(state);
537
+
538
+ if (fmRange) {
539
+ const cursorInFrontmatter = cursorLine >= fmRange.startLine && cursorLine <= fmRange.endLine;
540
+ const contentHash = 'fm-' + hashContent(fmRange.content);
541
+
542
+ if (!cursorInFrontmatter) {
543
+ decorations.push(
544
+ Decoration.replace({
545
+ widget: new FrontmatterWidgetWithHeightCache(
546
+ fmRange.content,
547
+ contentHash,
548
+ fmRange.from,
549
+ fmRange.to
550
+ ),
551
+ }).range(fmRange.from, fmRange.to)
552
+ );
553
+ } else {
554
+ // Cursor inside: show raw YAML with spacer for stable height
555
+ const cachedHeight = getCachedHeight(contentHash);
556
+ if (cachedHeight) {
557
+ const lineCount = fmRange.endLine - fmRange.startLine + 1;
558
+ const lineHeight = getLineHeight();
559
+ const rawHeight = lineCount * lineHeight;
560
+ const padding = cachedHeight - rawHeight;
561
+
562
+ if (padding > 0) {
563
+ const lastLine = doc.line(fmRange.endLine);
564
+ decorations.push(
565
+ Decoration.line({
566
+ attributes: {
567
+ class: 'cm-block-spacer-line',
568
+ style: `padding-bottom: ${padding}px`
569
+ }
570
+ }).range(lastLine.from)
571
+ );
572
+ }
573
+ }
574
+ }
575
+ }
576
+
474
577
  return Decoration.set(decorations, true);
475
578
  }
476
579
 
@@ -121,6 +121,33 @@ function extractWikiLinks(text) {
121
121
  return results;
122
122
  }
123
123
 
124
+ /**
125
+ * Find YAML frontmatter at the top of the document (--- ... ---).
126
+ *
127
+ * We use this to suppress markdown rendering inside frontmatter since the
128
+ * opening/closing `---` lines are parsed as HorizontalRule nodes by markdown.
129
+ */
130
+ function findFrontmatterRange(doc) {
131
+ if (doc.lines < 2) return null;
132
+
133
+ const firstLine = doc.line(1);
134
+ if (firstLine.text.trim() !== '---') return null;
135
+
136
+ for (let i = 2; i <= doc.lines; i++) {
137
+ const line = doc.line(i);
138
+ if (line.text.trim() === '---') {
139
+ return {
140
+ from: firstLine.from,
141
+ to: line.to,
142
+ startLine: 1,
143
+ endLine: i,
144
+ };
145
+ }
146
+ }
147
+
148
+ return null;
149
+ }
150
+
124
151
  /**
125
152
  * BlockImageWidget wrapper that caches its rendered height for stable layout.
126
153
  */
@@ -223,6 +250,7 @@ function buildDecorations(view) {
223
250
  const doc = view.state.doc;
224
251
  const cursorPos = view.state.selection.main.head;
225
252
  const cursorLine = doc.lineAt(cursorPos).number;
253
+ const frontmatterRange = findFrontmatterRange(doc);
226
254
 
227
255
  // Get asset resolver from facet (may be null)
228
256
  const assetResolver = view.state.facet(assetResolverFacet);
@@ -249,6 +277,21 @@ function buildDecorations(view) {
249
277
  to: view.viewport.to,
250
278
  enter: (node) => {
251
279
  const lineNum = doc.lineAt(node.from).number;
280
+
281
+ // Never apply markdown rendering/styling inside YAML frontmatter.
282
+ // Frontmatter is rendered separately by block-decorations.
283
+ // Use line-based start detection so we still skip nodes that may extend
284
+ // past the line boundary (e.g., HorizontalRule tokens including newline).
285
+ if (frontmatterRange && node.name !== 'Document') {
286
+ const nodeStartLine = doc.lineAt(node.from).number;
287
+ if (
288
+ nodeStartLine >= frontmatterRange.startLine &&
289
+ nodeStartLine <= frontmatterRange.endLine
290
+ ) {
291
+ return false;
292
+ }
293
+ }
294
+
252
295
  const isActiveLine = lineNum === cursorLine;
253
296
 
254
297
  // Marker class: hidden on blur, muted on focus
@@ -735,6 +778,9 @@ function buildDecorations(view) {
735
778
  const line = doc.line(i);
736
779
  const isActiveLine = i === cursorLine;
737
780
 
781
+ // Skip frontmatter lines
782
+ if (frontmatterRange && i >= frontmatterRange.startLine && i <= frontmatterRange.endLine) continue;
783
+
738
784
  // Skip lines inside code blocks
739
785
  if (codeBlockLines.has(i)) continue;
740
786
 
@@ -770,6 +816,9 @@ function buildDecorations(view) {
770
816
  const line = doc.line(i);
771
817
  const isActiveLine = i === cursorLine;
772
818
 
819
+ // Skip frontmatter lines
820
+ if (frontmatterRange && i >= frontmatterRange.startLine && i <= frontmatterRange.endLine) continue;
821
+
773
822
  // Skip lines inside code blocks (using syntax tree detection)
774
823
  if (codeBlockLines.has(i)) continue;
775
824
 
@@ -810,12 +859,12 @@ function buildDecorations(view) {
810
859
  const line = doc.line(i);
811
860
  const isActiveLine = i === cursorLine;
812
861
 
862
+ // Skip frontmatter lines
863
+ if (frontmatterRange && i >= frontmatterRange.startLine && i <= frontmatterRange.endLine) continue;
864
+
813
865
  // Skip lines inside code blocks (using syntax tree detection)
814
866
  if (codeBlockLines.has(i)) continue;
815
867
 
816
- // Skip frontmatter (YAML between ---)
817
- // This is a simple check - a full solution would track state
818
-
819
868
  const htmlElements = extractHtmlElements(line.text);
820
869
 
821
870
  for (const el of htmlElements) {
@@ -1015,6 +1015,132 @@ export const markdownStyles = `
1015
1015
  .cm-inline-html bdo {
1016
1016
  unicode-bidi: bidi-override;
1017
1017
  }
1018
+
1019
+ /* ==========================================================================
1020
+ MOBILE RESPONSIVE
1021
+
1022
+ Quarto/Astro-inspired: content reads beautifully on narrow screens.
1023
+ Tables scroll horizontally, images scale, code wraps or scrolls.
1024
+ ========================================================================== */
1025
+
1026
+ @media (max-width: 768px) {
1027
+
1028
+ /* Tables: horizontal scroll when they're wider than the viewport.
1029
+ This is the exact pattern Quarto and MkDocs Material use. */
1030
+ .cm-table-widget {
1031
+ overflow-x: auto;
1032
+ -webkit-overflow-scrolling: touch;
1033
+ margin-left: -4px;
1034
+ margin-right: -4px;
1035
+ padding-left: 4px;
1036
+ padding-right: 4px;
1037
+ }
1038
+
1039
+ /* Subtle fade on the right edge when table overflows */
1040
+ .cm-table-widget::after {
1041
+ content: '';
1042
+ position: sticky;
1043
+ right: 0;
1044
+ display: block;
1045
+ width: 16px;
1046
+ margin-top: -100%;
1047
+ height: 100%;
1048
+ background: linear-gradient(to right, transparent, var(--editor-background, #fff) 80%);
1049
+ pointer-events: none;
1050
+ float: right;
1051
+ opacity: 0.5;
1052
+ }
1053
+
1054
+ .cm-table {
1055
+ font-size: 0.88em;
1056
+ }
1057
+
1058
+ .cm-table th,
1059
+ .cm-table td {
1060
+ padding: 0.4em 0.65em;
1061
+ white-space: nowrap;
1062
+ }
1063
+
1064
+ /* Images: full-width, never overflow the screen */
1065
+ .cm-image-block-img {
1066
+ max-width: 100%;
1067
+ max-height: 50vh;
1068
+ }
1069
+
1070
+ .cm-image-inline-img {
1071
+ max-width: 100%;
1072
+ }
1073
+
1074
+ /* Wide images: don't break out of viewport */
1075
+ .cm-image-pos-wide {
1076
+ width: 100%;
1077
+ max-width: 100%;
1078
+ margin-left: 0;
1079
+ margin-right: 0;
1080
+ }
1081
+
1082
+ .cm-image-pos-wide .cm-image-block-img {
1083
+ max-width: 100%;
1084
+ }
1085
+
1086
+ /* Right/left aligned images: go full width on mobile */
1087
+ .cm-image-pos-right,
1088
+ .cm-image-pos-left {
1089
+ text-align: center;
1090
+ padding-left: 0;
1091
+ padding-right: 0;
1092
+ }
1093
+
1094
+ .cm-image-pos-right .cm-image-block-img,
1095
+ .cm-image-pos-left .cm-image-block-img {
1096
+ max-width: 100%;
1097
+ }
1098
+
1099
+ /* Display math: horizontal scroll for wide equations */
1100
+ .cm-math-display {
1101
+ overflow-x: auto;
1102
+ -webkit-overflow-scrolling: touch;
1103
+ }
1104
+
1105
+ /* Blockquotes: slightly tighter */
1106
+ .cm-md-blockquote-line {
1107
+ padding-left: 0.75em;
1108
+ }
1109
+
1110
+ /* Headings: slightly smaller on mobile for better fit */
1111
+ .cm-md-h1 {
1112
+ font-size: clamp(1.4em, 5vw, var(--md-heading-1-size, 1.75em));
1113
+ }
1114
+
1115
+ .cm-md-h2 {
1116
+ font-size: clamp(1.2em, 4vw, var(--md-heading-2-size, 1.4em));
1117
+ }
1118
+
1119
+ /* Horizontal rule: full width */
1120
+ .cm-md-hr-line::after {
1121
+ left: 0;
1122
+ right: 0;
1123
+ }
1124
+ }
1125
+
1126
+ /* Touch-specific: make interactive elements more tappable */
1127
+ @media (pointer: coarse) {
1128
+
1129
+ /* Checkboxes: bigger for finger tapping */
1130
+ .cm-task-checkbox {
1131
+ width: 1.2em;
1132
+ height: 1.2em;
1133
+ margin-right: 0.5em;
1134
+ }
1135
+
1136
+ /* Links: slightly more padding to enlarge tap area */
1137
+ .cm-md-link-text,
1138
+ .cm-external-link,
1139
+ .cm-file-link,
1140
+ .cm-wiki-link {
1141
+ padding: 2px 0;
1142
+ }
1143
+ }
1018
1144
  `;
1019
1145
 
1020
1146
  /**