@37signals/lexxy 0.8.5-beta → 0.9.0-beta

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.
@@ -50,6 +50,10 @@ function generateDomId(prefix) {
50
50
  return `${prefix}-${randomPart}`
51
51
  }
52
52
 
53
+ function extractPlainTextFromHtml(innerHtml = "") {
54
+ return parseHtml(innerHtml).body.textContent.trim()
55
+ }
56
+
53
57
  function highlightCode() {
54
58
  const elements = document.querySelectorAll("pre[data-language]");
55
59
 
@@ -65,12 +69,123 @@ function highlightElement(preElement) {
65
69
  const grammar = Prism.languages?.[language];
66
70
  if (!grammar) return
67
71
 
72
+ // Extract highlight ranges before Prism destroys <mark> elements
73
+ const highlights = extractHighlightRanges(preElement);
74
+
68
75
  // unescape HTML entities in the code block
69
76
  code = new DOMParser().parseFromString(code, "text/html").body.textContent || "";
70
77
 
71
78
  const highlightedHtml = Prism.highlight(code, grammar, language);
72
79
  const codeElement = createElement("code", { "data-language": language, innerHTML: highlightedHtml });
80
+
81
+ if (highlights.length > 0) {
82
+ applyHighlightRanges(codeElement, highlights);
83
+ }
84
+
73
85
  preElement.replaceWith(codeElement);
74
86
  }
75
87
 
76
- export { addBlockSpacing, createAttachmentFigure, createElement, dispatch, generateDomId, highlightCode, isPreviewableImage, parseHtml };
88
+ // Walk the DOM tree inside a <pre> element and build a list of
89
+ // { start, end, style } ranges for every <mark> element found.
90
+ function extractHighlightRanges(preElement) {
91
+ const ranges = [];
92
+ const root = preElement.querySelector("code") || preElement;
93
+
94
+ let offset = 0;
95
+
96
+ function walk(node) {
97
+ if (node.nodeType === Node.TEXT_NODE) {
98
+ offset += node.textContent.length;
99
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
100
+ if (node.tagName === "BR") {
101
+ offset += 1;
102
+ return
103
+ }
104
+
105
+ const isMark = node.tagName === "MARK";
106
+ const start = offset;
107
+
108
+ for (const child of node.childNodes) {
109
+ walk(child);
110
+ }
111
+
112
+ if (isMark) {
113
+ const style = extractStyle(node);
114
+ if (style) {
115
+ ranges.push({ start, end: offset, style });
116
+ }
117
+ }
118
+ }
119
+ }
120
+
121
+ for (const child of root.childNodes) {
122
+ walk(child);
123
+ }
124
+
125
+ return ranges
126
+ }
127
+
128
+ function extractStyle(element) {
129
+ const parts = [];
130
+ if (element.style?.color) parts.push(`color: ${element.style.color};`);
131
+ if (element.style?.backgroundColor) parts.push(`background-color: ${element.style.backgroundColor};`);
132
+ return parts.length > 0 ? parts.join(" ") : null
133
+ }
134
+
135
+ // Wrap character ranges in <mark> elements within a Prism-highlighted DOM tree.
136
+ // Each range is applied independently, re-collecting text nodes each time to
137
+ // account for splits from previous ranges.
138
+ function applyHighlightRanges(element, highlights) {
139
+ for (const { start, end, style } of highlights) {
140
+ wrapRange(element, start, end, style);
141
+ }
142
+ }
143
+
144
+ function wrapRange(container, rangeStart, rangeEnd, style) {
145
+ const textNodes = collectTextNodes(container);
146
+
147
+ // Process in reverse so DOM mutations don't shift earlier text node offsets
148
+ for (let i = textNodes.length - 1; i >= 0; i--) {
149
+ const { node, start: nodeStart, end: nodeEnd } = textNodes[i];
150
+ const overlapStart = Math.max(rangeStart, nodeStart);
151
+ const overlapEnd = Math.min(rangeEnd, nodeEnd);
152
+ if (overlapStart >= overlapEnd) continue
153
+
154
+ const relStart = overlapStart - nodeStart;
155
+ const relEnd = overlapEnd - nodeStart;
156
+ const text = node.textContent;
157
+ const parent = node.parentNode;
158
+
159
+ const mark = document.createElement("mark");
160
+ mark.setAttribute("style", style);
161
+ mark.textContent = text.slice(relStart, relEnd);
162
+
163
+ if (relEnd < text.length) {
164
+ parent.insertBefore(document.createTextNode(text.slice(relEnd)), node.nextSibling);
165
+ }
166
+ parent.insertBefore(mark, node.nextSibling);
167
+
168
+ if (relStart > 0) {
169
+ node.textContent = text.slice(0, relStart);
170
+ } else {
171
+ parent.removeChild(node);
172
+ }
173
+ }
174
+ }
175
+
176
+ function collectTextNodes(root) {
177
+ const nodes = [];
178
+ let offset = 0;
179
+ const walker = document.createTreeWalker(root, 4 /* NodeFilter.SHOW_TEXT */);
180
+
181
+ let node;
182
+ while ((node = walker.nextNode())) {
183
+ const length = node.textContent.length;
184
+ nodes.push({ node, start: offset, end: offset + length });
185
+ offset += length;
186
+ }
187
+
188
+ return nodes
189
+ }
190
+
191
+ export { addBlockSpacing, createAttachmentFigure, createElement, dispatch, extractPlainTextFromHtml, generateDomId, highlightCode, isPreviewableImage, parseHtml };
@@ -55,6 +55,10 @@
55
55
  text-decoration: underline;
56
56
  }
57
57
 
58
+ .lexxy-content__strikethrough.lexxy-content__underline {
59
+ text-decoration: line-through underline;
60
+ }
61
+
58
62
  mark,
59
63
  .lexxy-content__highlight {
60
64
  background-color: transparent;
@@ -436,7 +436,16 @@
436
436
  padding: 2px;
437
437
  position: relative;
438
438
 
439
- &[data-attachments="false"] button[name="upload"]{
439
+ &[data-attachments="false"] button[name="image"],
440
+ &[data-attachments="false"] button[name="file"] {
441
+ display: none;
442
+ }
443
+
444
+ &[data-upload="file"] button[name="image"] {
445
+ display: none;
446
+ }
447
+
448
+ &[data-upload="image"] button[name="file"] {
440
449
  display: none;
441
450
  }
442
451
 
@@ -503,6 +512,25 @@
503
512
  user-select: none;
504
513
  -webkit-user-select: none;
505
514
 
515
+ &.lexxy-editor__toolbar-dropdown--chevron {
516
+ summary {
517
+ aspect-ratio: unset;
518
+ gap: 0.5ch;
519
+ grid-template-columns: 2fr 1fr;
520
+ padding-inline: 0.75ch;
521
+
522
+ &:after {
523
+ block-size: 0.3ch;
524
+ border-block-end: 2px solid currentcolor;
525
+ border-inline-end: 2px solid currentcolor;
526
+ content: "";
527
+ display: inline-block;
528
+ inline-size: 0.3ch;
529
+ transform: rotate(45deg);
530
+ }
531
+ }
532
+ }
533
+
506
534
  summary ~ * {
507
535
  background-color: var(--lexxy-color-canvas);
508
536
  border: 2px solid var(--lexxy-color-selected-hover);
@@ -541,6 +569,44 @@
541
569
  }
542
570
  }
543
571
 
572
+ .lexxy-editor__toolbar-dropdown-list {
573
+ border-start-start-radius: 0;
574
+ flex-direction: column;
575
+ gap: 0.1ch;
576
+ padding: 0.1ch;
577
+
578
+ button {
579
+ align-items: center;
580
+ display: flex;
581
+ flex-direction: row;
582
+ gap: 1ch;
583
+ padding: 1ch;
584
+
585
+ &[aria-pressed="true"] {
586
+ background-color: var(--lexxy-color-selected);
587
+
588
+ &:hover {
589
+ background-color: var(--lexxy-color-selected-hover);
590
+ }
591
+ }
592
+
593
+ span {
594
+ font-size: var(--lexxy-text-small);
595
+ }
596
+
597
+ svg {
598
+ block-size: 1lh;
599
+ inline-size: 1lh;
600
+ }
601
+ }
602
+
603
+ .separator {
604
+ background: var(--lexxy-color-ink-lighter);
605
+ block-size: 1px;
606
+ inline-size: 100%;
607
+ }
608
+ }
609
+
544
610
 
545
611
  /* --------------------------------------------------------------------------
546
612
  /* Overflow menu */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@37signals/lexxy",
3
- "version": "0.8.5-beta",
3
+ "version": "0.9.0-beta",
4
4
  "description": "Lexxy - A modern rich text editor for Rails.",
5
5
  "module": "dist/lexxy.esm.js",
6
6
  "type": "module",