@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.
- package/dist/lexxy.esm.js +1253 -1239
- package/dist/lexxy_helpers.esm.js +116 -1
- package/dist/stylesheets/lexxy-content.css +4 -0
- package/dist/stylesheets/lexxy-editor.css +67 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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 };
|
|
@@ -436,7 +436,16 @@
|
|
|
436
436
|
padding: 2px;
|
|
437
437
|
position: relative;
|
|
438
438
|
|
|
439
|
-
&[data-attachments="false"] button[name="
|
|
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 */
|