bedazzlr 0.1.1 → 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 -153
  3. package/package.json +6 -6
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,42 +1,85 @@
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
- import * as cstml from '@bablr/language-en-cstml';
7
5
  import { streamParse } from 'bablr';
8
6
 
9
7
  import { CloseNodeTag, LiteralTag, OpenNodeTag, ReferenceTag } from '@bablr/agast-helpers/symbols';
10
8
  import { printSelfClosingNodeTag } from '@bablr/agast-helpers/print';
11
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';
12
19
 
13
- const getCommonParent = (a, b) => {
14
- if (!a) return b;
15
- if (!b) return a;
20
+ let anchorStyle =
21
+ 'display: inline-block; position: relative; vertical-align: top; pointer-events: none;';
16
22
 
17
- 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;
18
56
  do {
19
- let position = node.compareDocumentPosition(b);
57
+ let position = rootNode.compareDocumentPosition(a);
20
58
  if (!position || position & 0x10) {
21
- return node;
59
+ break;
22
60
  }
23
- } while ((node = node.parentNode));
61
+ bnode = rootNode;
62
+ } while ((rootNode = rootNode.parentNode));
24
63
 
25
- return null;
64
+ return [anode, bnode];
26
65
  };
27
66
 
28
67
  const countIndents = (root) => {
29
68
  let el = root;
30
69
  let indents = 0;
31
70
  while (el) {
32
- if (el.getAttribute('type') === 'LeftOffset') {
33
- 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;
34
77
 
35
- while (el) {
78
+ while (el && el.getAttribute('name') === 'Indent') {
36
79
  indents++;
37
- el = el.previousElementSibling;
80
+ el = el.nextElementSibling;
38
81
  }
39
- return indents + 1;
82
+ return indents;
40
83
  } else {
41
84
  el =
42
85
  el.previousElementSibling ||
@@ -44,7 +87,7 @@ const countIndents = (root) => {
44
87
  }
45
88
  }
46
89
 
47
- return 0;
90
+ return Infinity;
48
91
  };
49
92
 
50
93
  let callbacks = {};
@@ -70,83 +113,89 @@ export const removeDocumentListeners = () => {
70
113
  }
71
114
  };
72
115
 
73
- export const highlightCode = (el) => {
74
- let tags = streamParse(cstml, 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
+ ]();
75
120
 
76
121
  let open;
77
122
  let referenceTag = null;
78
123
  let range = document.createRange();
79
- range.selectNodeContents(el.childNodes[0].lastChild);
124
+ range.selectNodeContents(el.lastChild);
80
125
  let stack = emptyStack;
81
126
 
82
127
  let bindingTag;
83
128
 
84
129
  let step = tags.next();
85
130
 
86
- outer: while (!step.done) {
131
+ while (!step.done) {
87
132
  let tag = step.value;
88
133
 
89
- switch (tag.type) {
90
- case OpenNodeTag: {
91
- range = range.cloneRange();
92
- range.setStart(el.childNodes[0].lastChild, 0);
93
- 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);
94
138
 
95
- open = tag;
139
+ open = tag;
96
140
 
97
- stack = stack.push({
98
- open,
99
- range,
100
- referenceTag,
101
- // langs: stack.value.langs.concat(bindingTag?.value.languagePath ?? []),
102
- });
103
- break;
104
- }
105
-
106
- case ReferenceTag: {
107
- referenceTag = tag;
108
- break;
141
+ if (tag.value.literalValue) {
142
+ range.setEnd(range.startContainer, range.endOffset + tag.value.literalValue.length);
109
143
  }
110
144
 
111
- case LiteralTag: {
112
- let { value } = tag;
113
- range.setEnd(range.startContainer, range.startOffset + value.length);
114
- break;
115
- }
145
+ stack = stack.push({
146
+ open,
147
+ range,
148
+ referenceTag,
149
+ // langs: stack.value.langs.concat(bindingTag?.value.segments ?? []),
150
+ });
151
+ }
116
152
 
117
- case CloseNodeTag: {
118
- if (stack.size > 1) {
119
- // if (doneFrame.langs.size) {
120
-
121
- let doneRange = range;
122
-
123
- let node = document.createElement('node');
124
- let names = classNames({
125
- escape: referenceTag.value.type === '@',
126
- token: open.value.flags.token,
127
- trivia: referenceTag.value.type === '#',
128
- hasGap: open.value.flags.hasGap,
129
- });
130
- node.setAttribute('type', open.value.type.description);
131
- if (names) {
132
- node.setAttribute('class', names);
133
- }
153
+ if (tag.type === ReferenceTag) {
154
+ referenceTag = tag;
155
+ }
134
156
 
135
- range.surroundContents(node);
136
- // } else {
137
- // 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
+ }
138
161
 
139
- // doneFrame.range.surroundContents(el);
140
- // }
162
+ if (tag.type === CloseNodeTag || (tag.type === OpenNodeTag && tag.value.selfClosing)) {
163
+ if (stack.size > 1) {
164
+ // if (doneFrame.langs.size) {
141
165
 
142
- stack = stack.pop();
143
- ({ open, range, referenceTag } = stack.value);
166
+ let doneRange = range;
144
167
 
145
- // range.setStart(doneRange.startContainer, doneRange.startOffset);
146
- range.setEnd(doneRange.endContainer, doneRange.endOffset);
147
- } else {
148
- 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);
149
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 {
150
199
  break;
151
200
  }
152
201
  }
@@ -160,60 +209,114 @@ export const highlightCode = (el) => {
160
209
  set(value) {
161
210
  let { 0: start, 1: end } = value;
162
211
 
163
- let commonParent = getCommonParent(start, end);
212
+ let newValue = value;
164
213
 
165
- if (store._selectedRange) {
166
- let prevParent = store._selectedRangeCommonParent;
167
- if (prevParent) {
168
- prevParent.classList.remove('selected');
169
- 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) =>
170
233
  val.startsWith('indent-depth-'),
171
234
  );
172
- prevParent.classList.remove(indentClass);
235
+ prevEl.classList.remove(indentClass);
173
236
 
174
- let prevAnchor = prevParent.childNodes[0];
175
- let child = [...prevAnchor.childNodes].find((child) =>
176
- child.classList.contains('tooltip'),
177
- );
178
- if (child) prevAnchor.removeChild(child);
237
+ let prevAnchor = prevEl.previousSibling;
238
+ if (prevAnchor?.tagName === 'A') {
239
+ prevAnchor.remove();
240
+ }
179
241
  }
242
+ } else {
243
+ document.getSelection().empty();
180
244
  }
181
- store._selectedRange = value;
245
+
246
+ store._selectedRange = newValue;
247
+ store._selectedSiblingRange = siblingRange;
182
248
  store._selectedRangeCommonParent = commonParent;
183
249
 
184
250
  if (commonParent) {
185
- commonParent.classList.add('selected');
186
- commonParent.classList.add('indent-depth-' + countIndents(commonParent));
251
+ let intrinsicSelection = false;
187
252
 
188
- let parentRange = document.createRange();
253
+ let indentDepth = Infinity;
189
254
 
190
- parentRange.selectNode(commonParent);
191
-
192
- document.getSelection().empty();
193
- 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
+ }
194
262
 
195
- let anchor;
196
- if (commonParent.childNodes[0]?.tagName === 'A') {
197
- anchor = commonParent.childNodes[0];
198
- } else {
199
- anchor = document.createElement('a');
200
- anchor.style = 'display: inline-block; position: relative; vertical-align: top';
201
- commonParent.prepend(anchor);
263
+ if (intrinsicSelection) {
264
+ store._selectedSiblingRange = [commonParent, commonParent];
202
265
  }
203
266
 
204
- let tooltip = document.createElement('span');
205
- tooltip.classList.add('tooltip');
206
- tooltip.prepend(
207
- printSelfClosingNodeTag(
208
- buildOpenNodeTag(
209
- commonParent.classList.contains('token') ? tokenFlags : nodeFlags,
210
- 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
+ ),
211
295
  ),
212
- ),
213
- );
214
- 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
+ }
215
315
 
216
- anchor.prepend(tooltip);
316
+ // select siblings
317
+ document.getSelection().empty();
318
+ document.getSelection().addRange(range);
319
+ }
217
320
  }
218
321
  },
219
322
 
@@ -227,9 +330,9 @@ export const highlightCode = (el) => {
227
330
  let el = value;
228
331
 
229
332
  if (store._hoverTarget) {
230
- let prevEl = store._hoverTarget.childNodes[0];
231
- let child = [...prevEl.childNodes].find((child) => child.classList.contains('hover'));
232
- if (child) prevEl.removeChild(child);
333
+ let prevEl = store._hoverTarget;
334
+ let anchor = prevEl.previousSibling;
335
+ if (anchor.tagName === 'A') prevEl.parentElement.removeChild(anchor);
233
336
  }
234
337
 
235
338
  store._hoverTarget = value;
@@ -240,12 +343,13 @@ export const highlightCode = (el) => {
240
343
  hover.style = `position: absolute; top: 0px`;
241
344
 
242
345
  let anchor;
243
- if (el.childNodes[0]?.tagName === 'A') {
244
- anchor = el.childNodes[0];
346
+ if (el.previousSibling?.tagName === 'A') {
347
+ anchor = el.previousSibling;
245
348
  } else {
246
349
  anchor = document.createElement('a');
247
- anchor.style = 'display: inline-block; position: relative; vertical-align: top';
248
- el.prepend(anchor);
350
+ anchor.style = anchorStyle;
351
+
352
+ el.before(anchor);
249
353
  }
250
354
 
251
355
  anchor.prepend(hover);
@@ -277,11 +381,8 @@ export const highlightCode = (el) => {
277
381
  if (store.selectionState === 'selecting') {
278
382
  let selected = store.selectedRange;
279
383
 
280
- if (e.fromElement?.tagName !== 'NODE' && store.outTimeout) {
384
+ if (e.fromElement?.tagName !== 'NODE') {
281
385
  if (e.toElement?.tagName === 'NODE') {
282
- window.clearTimeout(store.outTimeout);
283
- store.outTimeout = null;
284
-
285
386
  let startTokenNode = selected[0];
286
387
 
287
388
  if (startTokenNode) {
@@ -292,7 +393,7 @@ export const highlightCode = (el) => {
292
393
 
293
394
  store.selectedRange = range;
294
395
  } else {
295
- store.selectedRange = [selected[0], selected[0]];
396
+ store.selectedRange = [selected[0], null];
296
397
  }
297
398
  }
298
399
  }
@@ -307,57 +408,54 @@ export const highlightCode = (el) => {
307
408
  el.addEventListener('mouseout', (e) => {
308
409
  if (store.selectionState === 'selecting') {
309
410
  let selected = store.selectedRange;
310
- if (store.outTimeout) {
311
- window.clearTimeout(store.outTimeout);
312
- store.outTimeout = null;
313
- }
314
411
 
315
- if (e.toElement.tagName === 'NODE') {
412
+ if (e.toElement?.tagName === 'NODE') {
316
413
  let range;
317
414
 
318
- if (e.target.tagName === 'NODE') {
319
- window.clearTimeout(store.outTimeout);
320
- store.outTimeout = null;
321
- }
322
-
323
415
  let startTokenNode = selected[0];
324
416
 
325
417
  if (startTokenNode) {
326
418
  range = [startTokenNode, e.toElement];
327
419
  } else {
328
- range = [e.toElement, e.toElement];
420
+ range = [e.toElement, null];
329
421
  }
330
422
 
331
423
  store.selectedRange = range;
332
424
  } else {
333
- // with tall line-height there are little gaps between lines where the event lands on `el`
334
- // suppress strobe-like flashing that occurs while dragging a selection over these gaps
335
- store.outTimeout = window.setTimeout(() => {
336
- if (e.toElement.tagName === 'NODE') {
337
- store.selectedRange = [store.selectedRange[0], store.selectedRange[0]];
338
- } else {
339
- store.selectedRange = [store.selectedRange[0], null];
340
- }
341
- }, 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
+ }
342
430
  }
343
431
  }
344
432
  });
345
433
 
346
- el.addEventListener('mouseup', (e) => {
347
- store.selectionState = store.selectedRange ? 'selected' : 'none';
348
- });
349
-
350
- addDocumentEventListener('selectionchanged', (e) => {
351
- let selection = document.getSelection();
434
+ addDocumentEventListener('mouseout', (e) => {
435
+ let { target } = e;
352
436
 
353
- if (selection.isCollapsed) {
354
- if (!selection.anchorNode || !(selection.anchorNode.compareDocumentPosition(el) & 0x10)) {
355
- store.selectedRange = [null, null];
437
+ if (store.hoverTarget) {
438
+ let position = target.compareDocumentPosition(el);
439
+ if (!position || position & 0x10) {
440
+ store.hoverTarget = null;
356
441
  }
357
442
  }
358
443
  });
359
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
+
360
457
  addDocumentEventListener('mouseup', (e) => {
458
+ store.selectionState = store.selectedRange ? 'selected' : 'none';
361
459
  if (e.target.compareDocumentPosition(el) & 0x10) {
362
460
  store.selectedRange = [null, null];
363
461
  document.getSelection().empty();
@@ -373,10 +471,37 @@ export const highlightCode = (el) => {
373
471
  });
374
472
  };
375
473
 
376
- export const highlightAll = () => {
474
+ export const highlightAll = (languages) => {
377
475
  let codeBlocks = document.querySelectorAll('code');
378
476
 
379
477
  for (let block of codeBlocks) {
380
- 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
+ }
381
506
  }
382
507
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bedazzlr",
3
- "version": "0.1.1",
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,12 +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",
21
- "@bablr/language-en-cstml": "0.11.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",
22
22
  "@iter-tools/imm-stack": "1.2.0",
23
- "bablr": "0.10.1",
23
+ "bablr": "0.11.2",
24
24
  "classnames": "2.5.1"
25
25
  },
26
26
  "devDependencies": {