bedazzlr 0.2.0 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/lib/index.js +278 -152
  3. package/package.json +6 -5
package/README.md CHANGED
@@ -1,3 +1,3 @@
1
1
  ## bedazzlr
2
2
 
3
- Syntax highlight for `<code />` blocks on webpages powered by BABLR
3
+ Syntax highlighter for `<code />` blocks on webpages powered by BABLR
package/lib/index.js CHANGED
@@ -1,6 +1,5 @@
1
- /* global document window */
1
+ /* global document window console */
2
2
 
3
- import { spam as m } from '@bablr/boot';
4
3
  import emptyStack from '@iter-tools/imm-stack';
5
4
  import classNames from 'classnames';
6
5
  import { streamParse } from 'bablr';
@@ -8,34 +7,79 @@ import { streamParse } from 'bablr';
8
7
  import { CloseNodeTag, LiteralTag, OpenNodeTag, ReferenceTag } from '@bablr/agast-helpers/symbols';
9
8
  import { printSelfClosingNodeTag } from '@bablr/agast-helpers/print';
10
9
  import { buildOpenNodeTag, nodeFlags, tokenFlags } from '@bablr/agast-helpers/builders';
10
+ import {
11
+ buildTreeNodeMatcher,
12
+ buildNodeFlags,
13
+ buildTreeNodeMatcherOpen,
14
+ buildPropertyMatcher,
15
+ buildBoundNodeMatcher,
16
+ buildReferenceMatcher,
17
+ } from '@bablr/helpers/builders';
18
+ import { buildEmbeddedMatcher } from '@bablr/agast-vm-helpers/builders';
11
19
 
12
- const getCommonParent = (a, b) => {
13
- if (!a) return b;
14
- if (!b) return a;
20
+ let anchorStyle =
21
+ 'display: inline-block; position: relative; vertical-align: top; pointer-events: none;';
15
22
 
16
- let node = a;
23
+ function* siblingsFor(range) {
24
+ let { 0: left, 1: right } = range;
25
+ let el = left || right;
26
+
27
+ if (!el) return;
28
+
29
+ let backwards = left.compareDocumentPosition(right) === 2;
30
+
31
+ do {
32
+ yield el;
33
+ let nextSibling = backwards ? el.previousElementSibling : el.nextElementSibling;
34
+ if (!right || el === right || !nextSibling) break;
35
+ el = nextSibling;
36
+ } while (true);
37
+ }
38
+
39
+ const getCommonParentSiblings = (a, b) => {
40
+ if (!a && !b) return [];
41
+ if (!a) return [b, b];
42
+ if (!b || a === b) return [a, a];
43
+
44
+ let anode = a;
45
+ let bnode = b;
46
+ let rootNode = anode;
47
+ do {
48
+ let position = rootNode.compareDocumentPosition(b);
49
+ if (!position || position & 0x10) {
50
+ break;
51
+ }
52
+ anode = rootNode;
53
+ } while ((rootNode = rootNode.parentNode));
54
+
55
+ rootNode = bnode;
17
56
  do {
18
- let position = node.compareDocumentPosition(b);
57
+ let position = rootNode.compareDocumentPosition(a);
19
58
  if (!position || position & 0x10) {
20
- return node;
59
+ break;
21
60
  }
22
- } while ((node = node.parentNode));
61
+ bnode = rootNode;
62
+ } while ((rootNode = rootNode.parentNode));
23
63
 
24
- return null;
64
+ return [anode, bnode];
25
65
  };
26
66
 
27
67
  const countIndents = (root) => {
28
68
  let el = root;
29
69
  let indents = 0;
30
70
  while (el) {
31
- if (el.getAttribute('type') === 'LeftOffset') {
32
- el = el.lastElementChild;
71
+ if (el.classList.contains('trivia')) {
72
+ let leftOffset = el.firstElementChild;
73
+
74
+ if (leftOffset?.getAttribute('name') !== 'LeftOffset') return Infinity;
75
+
76
+ el = leftOffset.firstElementChild;
33
77
 
34
- while (el) {
78
+ while (el && el.getAttribute('name') === 'Indent') {
35
79
  indents++;
36
- el = el.previousElementSibling;
80
+ el = el.nextElementSibling;
37
81
  }
38
- return indents + 1;
82
+ return indents;
39
83
  } else {
40
84
  el =
41
85
  el.previousElementSibling ||
@@ -43,7 +87,7 @@ const countIndents = (root) => {
43
87
  }
44
88
  }
45
89
 
46
- return 0;
90
+ return Infinity;
47
91
  };
48
92
 
49
93
  let callbacks = {};
@@ -69,83 +113,89 @@ export const removeDocumentListeners = () => {
69
113
  }
70
114
  };
71
115
 
72
- export const highlightCode = (language, el) => {
73
- let tags = streamParse(language, m`<Document />`, el.innerText)[Symbol.iterator]();
116
+ export const highlightCode = (el, language, matcher = language.defaultMatcher) => {
117
+ let tags = streamParse(language, matcher, el.innerText, null, { holdShiftedNodes: true })[
118
+ Symbol.iterator
119
+ ]();
74
120
 
75
121
  let open;
76
122
  let referenceTag = null;
77
123
  let range = document.createRange();
78
- range.selectNodeContents(el.childNodes[0].lastChild);
124
+ range.selectNodeContents(el.lastChild);
79
125
  let stack = emptyStack;
80
126
 
81
127
  let bindingTag;
82
128
 
83
129
  let step = tags.next();
84
130
 
85
- outer: while (!step.done) {
131
+ while (!step.done) {
86
132
  let tag = step.value;
87
133
 
88
- switch (tag.type) {
89
- case OpenNodeTag: {
90
- range = range.cloneRange();
91
- range.setStart(el.childNodes[0].lastChild, 0);
92
- range.setEnd(el.childNodes[0].lastChild, 0);
134
+ if (tag.type === OpenNodeTag) {
135
+ range = range.cloneRange();
136
+ range.setStart(el.lastChild, 0);
137
+ range.setEnd(el.lastChild, 0);
93
138
 
94
- open = tag;
139
+ open = tag;
95
140
 
96
- stack = stack.push({
97
- open,
98
- range,
99
- referenceTag,
100
- // langs: stack.value.langs.concat(bindingTag?.value.languagePath ?? []),
101
- });
102
- break;
103
- }
104
-
105
- case ReferenceTag: {
106
- referenceTag = tag;
107
- break;
141
+ if (tag.value.literalValue) {
142
+ range.setEnd(range.startContainer, range.endOffset + tag.value.literalValue.length);
108
143
  }
109
144
 
110
- case LiteralTag: {
111
- let { value } = tag;
112
- range.setEnd(range.startContainer, range.startOffset + value.length);
113
- break;
114
- }
145
+ stack = stack.push({
146
+ open,
147
+ range,
148
+ referenceTag,
149
+ // langs: stack.value.langs.concat(bindingTag?.value.segments ?? []),
150
+ });
151
+ }
115
152
 
116
- case CloseNodeTag: {
117
- if (stack.size > 1) {
118
- // if (doneFrame.langs.size) {
119
-
120
- let doneRange = range;
121
-
122
- let node = document.createElement('node');
123
- let names = classNames({
124
- escape: referenceTag.value.type === '@',
125
- token: open.value.flags.token,
126
- trivia: referenceTag.value.type === '#',
127
- hasGap: open.value.flags.hasGap,
128
- });
129
- node.setAttribute('type', open.value.type.description);
130
- if (names) {
131
- node.setAttribute('class', names);
132
- }
153
+ if (tag.type === ReferenceTag) {
154
+ referenceTag = tag;
155
+ }
133
156
 
134
- range.surroundContents(node);
135
- // } else {
136
- // let el = document.createElement(doneFrame.type.description);
157
+ if (tag.type === LiteralTag) {
158
+ let { value } = tag;
159
+ range.setEnd(range.startContainer, range.endOffset + value.length);
160
+ }
137
161
 
138
- // doneFrame.range.surroundContents(el);
139
- // }
162
+ if (tag.type === CloseNodeTag || (tag.type === OpenNodeTag && tag.value.selfClosing)) {
163
+ if (stack.size > 1) {
164
+ // if (doneFrame.langs.size) {
140
165
 
141
- stack = stack.pop();
142
- ({ open, range, referenceTag } = stack.value);
166
+ let doneRange = range;
143
167
 
144
- // range.setStart(doneRange.startContainer, doneRange.startOffset);
145
- range.setEnd(doneRange.endContainer, doneRange.endOffset);
146
- } else {
147
- break outer;
168
+ let node = document.createElement('node');
169
+ let names = classNames({
170
+ escape: referenceTag?.value.type === '@',
171
+ token: open.value.flags.token,
172
+ trivia: referenceTag?.value.type === '#',
173
+ intrinsic: referenceTag?.value.flags.intrinsic || false,
174
+ hasGap: open.value.flags.hasGap,
175
+ });
176
+ if (open.value.name) {
177
+ node.setAttribute('name', open.value.name?.description);
178
+ }
179
+ if (open.value.name) {
180
+ node.setAttribute('name', open.value.name?.description);
181
+ }
182
+ if (names) {
183
+ node.setAttribute('class', names);
148
184
  }
185
+
186
+ range.surroundContents(node);
187
+ // } else {
188
+ // let el = document.createElement(doneFrame.name.description);
189
+
190
+ // doneFrame.range.surroundContents(el);
191
+ // }
192
+
193
+ stack = stack.pop();
194
+ ({ open, range, referenceTag } = stack.value);
195
+
196
+ // range.setStart(doneRange.startContainer, doneRange.startOffset);
197
+ range.setEnd(doneRange.endContainer, doneRange.endOffset);
198
+ } else {
149
199
  break;
150
200
  }
151
201
  }
@@ -159,60 +209,114 @@ export const highlightCode = (language, el) => {
159
209
  set(value) {
160
210
  let { 0: start, 1: end } = value;
161
211
 
162
- let commonParent = getCommonParent(start, end);
212
+ let newValue = value;
163
213
 
164
- if (store._selectedRange) {
165
- let prevParent = store._selectedRangeCommonParent;
166
- if (prevParent) {
167
- prevParent.classList.remove('selected');
168
- let indentClass = [...prevParent.classList.values()].find((val) =>
214
+ if (start && !end) {
215
+ end = store._selectedRange?.[1];
216
+ newValue = [start, end];
217
+ }
218
+
219
+ let siblingRange = getCommonParentSiblings(start, end);
220
+
221
+ let { 0: leftBound, 1: rightBound } = siblingRange;
222
+ let commonParent =
223
+ leftBound === rightBound || !rightBound
224
+ ? leftBound
225
+ : leftBound?.parentNode || rightBound?.parentNode;
226
+
227
+ // are any selected nodes intrinsic
228
+
229
+ if (store._selectedSiblingRange) {
230
+ for (let prevEl of siblingsFor(store._selectedSiblingRange)) {
231
+ prevEl.classList.remove('selected');
232
+ let indentClass = [...prevEl.classList.values()].find((val) =>
169
233
  val.startsWith('indent-depth-'),
170
234
  );
171
- prevParent.classList.remove(indentClass);
235
+ prevEl.classList.remove(indentClass);
172
236
 
173
- let prevAnchor = prevParent.childNodes[0];
174
- let child = [...prevAnchor.childNodes].find((child) =>
175
- child.classList.contains('tooltip'),
176
- );
177
- if (child) prevAnchor.removeChild(child);
237
+ let prevAnchor = prevEl.previousSibling;
238
+ if (prevAnchor?.tagName === 'A') {
239
+ prevAnchor.remove();
240
+ }
178
241
  }
242
+ } else {
243
+ document.getSelection().empty();
179
244
  }
180
- store._selectedRange = value;
245
+
246
+ store._selectedRange = newValue;
247
+ store._selectedSiblingRange = siblingRange;
181
248
  store._selectedRangeCommonParent = commonParent;
182
249
 
183
250
  if (commonParent) {
184
- commonParent.classList.add('selected');
185
- commonParent.classList.add('indent-depth-' + countIndents(commonParent));
251
+ let intrinsicSelection = false;
186
252
 
187
- let parentRange = document.createRange();
253
+ let indentDepth = Infinity;
188
254
 
189
- parentRange.selectNode(commonParent);
190
-
191
- document.getSelection().empty();
192
- document.getSelection().addRange(parentRange);
255
+ for (let siblingNode of siblingsFor(siblingRange)) {
256
+ intrinsicSelection =
257
+ siblingNode.classList.contains('intrinsic') &&
258
+ !siblingNode.classList.contains('trivia');
259
+ indentDepth = Math.min(indentDepth, countIndents(siblingNode));
260
+ if (intrinsicSelection) break;
261
+ }
193
262
 
194
- let anchor;
195
- if (commonParent.childNodes[0]?.tagName === 'A') {
196
- anchor = commonParent.childNodes[0];
197
- } else {
198
- anchor = document.createElement('a');
199
- anchor.style = 'display: inline-block; position: relative; vertical-align: top';
200
- commonParent.prepend(anchor);
263
+ if (intrinsicSelection) {
264
+ store._selectedSiblingRange = [commonParent, commonParent];
201
265
  }
202
266
 
203
- let tooltip = document.createElement('span');
204
- tooltip.classList.add('tooltip');
205
- tooltip.prepend(
206
- printSelfClosingNodeTag(
207
- buildOpenNodeTag(
208
- commonParent.classList.contains('token') ? tokenFlags : nodeFlags,
209
- commonParent.getAttribute('type'),
267
+ if (!leftBound || !rightBound || intrinsicSelection) {
268
+ commonParent.classList.add('selected');
269
+ commonParent.classList.add('indent-depth-' + countIndents(commonParent));
270
+
271
+ let anchor;
272
+ if (commonParent.previousSibling?.tagName === 'A') {
273
+ anchor = commonParent.previousSibling;
274
+ } else {
275
+ anchor = document.createElement('a');
276
+ anchor.style = anchorStyle;
277
+ commonParent.before(anchor);
278
+ }
279
+
280
+ let parentRange = document.createRange();
281
+
282
+ parentRange.selectNode(commonParent);
283
+
284
+ document.getSelection().empty();
285
+ document.getSelection().addRange(parentRange);
286
+
287
+ let tooltip = document.createElement('span');
288
+ tooltip.classList.add('tooltip');
289
+ tooltip.prepend(
290
+ printSelfClosingNodeTag(
291
+ buildOpenNodeTag(
292
+ commonParent.classList.contains('token') ? tokenFlags : nodeFlags,
293
+ commonParent.getAttribute('name'),
294
+ ),
210
295
  ),
211
- ),
212
- );
213
- tooltip.style = `position: absolute; top: -20px`;
296
+ );
297
+ tooltip.style = `position: absolute; top: -20px`;
298
+
299
+ anchor.prepend(tooltip);
300
+ } else {
301
+ for (let siblingNode of siblingsFor(siblingRange)) {
302
+ siblingNode.classList.add('selected');
303
+ siblingNode.classList.add('indent-depth-' + indentDepth);
304
+ }
305
+
306
+ let range = document.createRange();
307
+
308
+ if (leftBound.compareDocumentPosition(rightBound) === 2) {
309
+ range.setStartBefore(rightBound);
310
+ range.setEndAfter(leftBound);
311
+ } else {
312
+ range.setStartBefore(leftBound);
313
+ range.setEndAfter(rightBound);
314
+ }
214
315
 
215
- anchor.prepend(tooltip);
316
+ // select siblings
317
+ document.getSelection().empty();
318
+ document.getSelection().addRange(range);
319
+ }
216
320
  }
217
321
  },
218
322
 
@@ -226,9 +330,9 @@ export const highlightCode = (language, el) => {
226
330
  let el = value;
227
331
 
228
332
  if (store._hoverTarget) {
229
- let prevEl = store._hoverTarget.childNodes[0];
230
- let child = [...prevEl.childNodes].find((child) => child.classList.contains('hover'));
231
- if (child) prevEl.removeChild(child);
333
+ let prevEl = store._hoverTarget;
334
+ let anchor = prevEl.previousSibling;
335
+ if (anchor.tagName === 'A') prevEl.parentElement.removeChild(anchor);
232
336
  }
233
337
 
234
338
  store._hoverTarget = value;
@@ -239,12 +343,13 @@ export const highlightCode = (language, el) => {
239
343
  hover.style = `position: absolute; top: 0px`;
240
344
 
241
345
  let anchor;
242
- if (el.childNodes[0]?.tagName === 'A') {
243
- anchor = el.childNodes[0];
346
+ if (el.previousSibling?.tagName === 'A') {
347
+ anchor = el.previousSibling;
244
348
  } else {
245
349
  anchor = document.createElement('a');
246
- anchor.style = 'display: inline-block; position: relative; vertical-align: top';
247
- el.prepend(anchor);
350
+ anchor.style = anchorStyle;
351
+
352
+ el.before(anchor);
248
353
  }
249
354
 
250
355
  anchor.prepend(hover);
@@ -276,11 +381,8 @@ export const highlightCode = (language, el) => {
276
381
  if (store.selectionState === 'selecting') {
277
382
  let selected = store.selectedRange;
278
383
 
279
- if (e.fromElement?.tagName !== 'NODE' && store.outTimeout) {
384
+ if (e.fromElement?.tagName !== 'NODE') {
280
385
  if (e.toElement?.tagName === 'NODE') {
281
- window.clearTimeout(store.outTimeout);
282
- store.outTimeout = null;
283
-
284
386
  let startTokenNode = selected[0];
285
387
 
286
388
  if (startTokenNode) {
@@ -291,7 +393,7 @@ export const highlightCode = (language, el) => {
291
393
 
292
394
  store.selectedRange = range;
293
395
  } else {
294
- store.selectedRange = [selected[0], selected[0]];
396
+ store.selectedRange = [selected[0], null];
295
397
  }
296
398
  }
297
399
  }
@@ -306,57 +408,54 @@ export const highlightCode = (language, el) => {
306
408
  el.addEventListener('mouseout', (e) => {
307
409
  if (store.selectionState === 'selecting') {
308
410
  let selected = store.selectedRange;
309
- if (store.outTimeout) {
310
- window.clearTimeout(store.outTimeout);
311
- store.outTimeout = null;
312
- }
313
411
 
314
- if (e.toElement.tagName === 'NODE') {
412
+ if (e.toElement?.tagName === 'NODE') {
315
413
  let range;
316
414
 
317
- if (e.target.tagName === 'NODE') {
318
- window.clearTimeout(store.outTimeout);
319
- store.outTimeout = null;
320
- }
321
-
322
415
  let startTokenNode = selected[0];
323
416
 
324
417
  if (startTokenNode) {
325
418
  range = [startTokenNode, e.toElement];
326
419
  } else {
327
- range = [e.toElement, e.toElement];
420
+ range = [e.toElement, null];
328
421
  }
329
422
 
330
423
  store.selectedRange = range;
331
424
  } else {
332
- // with tall line-height there are little gaps between lines where the event lands on `el`
333
- // suppress strobe-like flashing that occurs while dragging a selection over these gaps
334
- store.outTimeout = window.setTimeout(() => {
335
- if (e.toElement.tagName === 'NODE') {
336
- store.selectedRange = [store.selectedRange[0], store.selectedRange[0]];
337
- } else {
338
- store.selectedRange = [store.selectedRange[0], null];
339
- }
340
- }, 65);
425
+ if (e.toElement?.tagName === 'NODE') {
426
+ store.selectedRange = [store.selectedRange[0], store.selectedRange[0]];
427
+ } else {
428
+ store.selectedRange = [store.selectedRange[0], null];
429
+ }
341
430
  }
342
431
  }
343
432
  });
344
433
 
345
- el.addEventListener('mouseup', (e) => {
346
- store.selectionState = store.selectedRange ? 'selected' : 'none';
347
- });
348
-
349
- addDocumentEventListener('selectionchanged', (e) => {
350
- let selection = document.getSelection();
434
+ addDocumentEventListener('mouseout', (e) => {
435
+ let { target } = e;
351
436
 
352
- if (selection.isCollapsed) {
353
- if (!selection.anchorNode || !(selection.anchorNode.compareDocumentPosition(el) & 0x10)) {
354
- store.selectedRange = [null, null];
437
+ if (store.hoverTarget) {
438
+ let position = target.compareDocumentPosition(el);
439
+ if (!position || position & 0x10) {
440
+ store.hoverTarget = null;
355
441
  }
356
442
  }
357
443
  });
358
444
 
445
+ // addDocumentEventListener('selectionchange', (e) => {
446
+ // let selection = document.getSelection();
447
+
448
+ // store.hoverTarget = selection.anchorNode;
449
+
450
+ // if (selection.isCollapsed) {
451
+ // if (!selection.anchorNode || !(selection.anchorNode.compareDocumentPosition(el) & 0x10)) {
452
+ // store.selectedRange = [null, null];
453
+ // }
454
+ // }
455
+ // });
456
+
359
457
  addDocumentEventListener('mouseup', (e) => {
458
+ store.selectionState = store.selectedRange ? 'selected' : 'none';
360
459
  if (e.target.compareDocumentPosition(el) & 0x10) {
361
460
  store.selectedRange = [null, null];
362
461
  document.getSelection().empty();
@@ -372,10 +471,37 @@ export const highlightCode = (language, el) => {
372
471
  });
373
472
  };
374
473
 
375
- export const highlightAll = () => {
474
+ export const highlightAll = (languages) => {
376
475
  let codeBlocks = document.querySelectorAll('code');
377
476
 
378
477
  for (let block of codeBlocks) {
379
- highlightCode(block);
478
+ let canonicalURL = block.getAttribute('bablr-lang');
479
+ let language = languages.get(canonicalURL);
480
+ let flags = block.getAttribute('bablr-ref-flags');
481
+
482
+ let name = block.getAttribute('bablr-prod');
483
+
484
+ if (!language) continue;
485
+ if (!name && !language.defaultMatcher) continue;
486
+
487
+ try {
488
+ highlightCode(
489
+ block,
490
+ language,
491
+ name
492
+ ? buildEmbeddedMatcher(
493
+ buildPropertyMatcher(
494
+ buildReferenceMatcher('_', null, flags),
495
+ buildBoundNodeMatcher(
496
+ [],
497
+ buildTreeNodeMatcher(buildTreeNodeMatcherOpen(buildNodeFlags(), null, name)),
498
+ ),
499
+ ),
500
+ )
501
+ : language.defaultMatcher,
502
+ );
503
+ } catch (e) {
504
+ console.warn(e);
505
+ }
380
506
  }
381
507
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bedazzlr",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Code block syntax highlighting using BABLR",
5
5
  "author": "Conrad Buck<conartist6@gmail.com>",
6
6
  "type": "module",
@@ -15,11 +15,12 @@
15
15
  "test": "mocha test/*.test.js"
16
16
  },
17
17
  "dependencies": {
18
- "@bablr/agast-helpers": "0.9.0",
19
- "@bablr/boot": "0.10.0",
20
- "@bablr/helpers": "0.24.0",
18
+ "@bablr/agast-helpers": "0.10.2",
19
+ "@bablr/agast-vm-helpers": "0.10.2",
20
+ "@bablr/boot": "0.11.1",
21
+ "@bablr/helpers": "0.25.1",
21
22
  "@iter-tools/imm-stack": "1.2.0",
22
- "bablr": "0.10.1",
23
+ "bablr": "0.11.2",
23
24
  "classnames": "2.5.1"
24
25
  },
25
26
  "devDependencies": {