bedazzlr 0.2.0 → 0.2.2

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