@syware/jlib 0.0.0-stage → 0.1.3
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/LICENSE +21 -0
- package/README.md +133 -2
- package/dist/all.d.ts +1 -0
- package/dist/all.js +75 -0
- package/dist/atoms/jl-avatar.css_raw.js +3 -0
- package/dist/atoms/jl-avatar.d.ts +52 -0
- package/dist/atoms/jl-avatar.js +167 -0
- package/dist/atoms/jl-badge.css_raw.js +3 -0
- package/dist/atoms/jl-badge.d.ts +32 -0
- package/dist/atoms/jl-badge.js +60 -0
- package/dist/atoms/jl-button.css_raw.js +3 -0
- package/dist/atoms/jl-button.d.ts +70 -0
- package/dist/atoms/jl-button.js +146 -0
- package/dist/atoms/jl-checkbox.css_raw.js +3 -0
- package/dist/atoms/jl-checkbox.d.ts +61 -0
- package/dist/atoms/jl-checkbox.js +149 -0
- package/dist/atoms/jl-chip.css_raw.js +3 -0
- package/dist/atoms/jl-chip.d.ts +59 -0
- package/dist/atoms/jl-chip.js +164 -0
- package/dist/atoms/jl-divider.css_raw.js +3 -0
- package/dist/atoms/jl-divider.d.ts +26 -0
- package/dist/atoms/jl-divider.js +59 -0
- package/dist/atoms/jl-icon.css_raw.js +3 -0
- package/dist/atoms/jl-icon.d.ts +25 -0
- package/dist/atoms/jl-icon.js +41 -0
- package/dist/atoms/jl-input.css_raw.js +3 -0
- package/dist/atoms/jl-input.d.ts +112 -0
- package/dist/atoms/jl-input.js +202 -0
- package/dist/atoms/jl-kbd.css_raw.js +3 -0
- package/dist/atoms/jl-kbd.d.ts +17 -0
- package/dist/atoms/jl-kbd.js +62 -0
- package/dist/atoms/jl-link.css_raw.js +3 -0
- package/dist/atoms/jl-link.d.ts +37 -0
- package/dist/atoms/jl-link.js +86 -0
- package/dist/atoms/jl-progress-ring.css_raw.js +3 -0
- package/dist/atoms/jl-progress-ring.d.ts +49 -0
- package/dist/atoms/jl-progress-ring.js +132 -0
- package/dist/atoms/jl-progress.css_raw.js +3 -0
- package/dist/atoms/jl-progress.d.ts +43 -0
- package/dist/atoms/jl-progress.js +109 -0
- package/dist/atoms/jl-rating.css_raw.js +3 -0
- package/dist/atoms/jl-rating.d.ts +73 -0
- package/dist/atoms/jl-rating.js +184 -0
- package/dist/atoms/jl-select.css_raw.js +3 -0
- package/dist/atoms/jl-select.d.ts +70 -0
- package/dist/atoms/jl-select.js +146 -0
- package/dist/atoms/jl-skeleton.css_raw.js +3 -0
- package/dist/atoms/jl-skeleton.d.ts +34 -0
- package/dist/atoms/jl-skeleton.js +63 -0
- package/dist/atoms/jl-slider.css_raw.js +3 -0
- package/dist/atoms/jl-slider.d.ts +65 -0
- package/dist/atoms/jl-slider.js +143 -0
- package/dist/atoms/jl-spinner.css_raw.js +3 -0
- package/dist/atoms/jl-spinner.d.ts +16 -0
- package/dist/atoms/jl-spinner.js +26 -0
- package/dist/atoms/jl-switch.css_raw.js +3 -0
- package/dist/atoms/jl-switch.d.ts +57 -0
- package/dist/atoms/jl-switch.js +125 -0
- package/dist/atoms/jl-textarea.css_raw.js +3 -0
- package/dist/atoms/jl-textarea.d.ts +81 -0
- package/dist/atoms/jl-textarea.js +148 -0
- package/dist/base/jl-element.d.ts +16 -0
- package/dist/base/jl-element.js +35 -0
- package/dist/base/jl-form-control.d.ts +29 -0
- package/dist/base/jl-form-control.js +44 -0
- package/dist/index.d.ts +119 -0
- package/dist/index.js +72 -0
- package/dist/lib/arrow.d.ts +6 -0
- package/dist/lib/arrow.js +15 -0
- package/dist/lib/attributes.d.ts +9 -0
- package/dist/lib/attributes.js +22 -0
- package/dist/lib/avatar.d.ts +11 -0
- package/dist/lib/avatar.js +37 -0
- package/dist/lib/calendar.d.ts +48 -0
- package/dist/lib/calendar.js +166 -0
- package/dist/lib/css-length.d.ts +2 -0
- package/dist/lib/css-length.js +8 -0
- package/dist/lib/define.d.ts +9 -0
- package/dist/lib/define.js +9 -0
- package/dist/lib/dom.d.ts +4 -0
- package/dist/lib/dom.js +16 -0
- package/dist/lib/files.d.ts +18 -0
- package/dist/lib/files.js +89 -0
- package/dist/lib/floating.d.ts +13 -0
- package/dist/lib/floating.js +127 -0
- package/dist/lib/fuzzy.d.ts +26 -0
- package/dist/lib/fuzzy.js +149 -0
- package/dist/lib/highlight.d.ts +5 -0
- package/dist/lib/highlight.js +267 -0
- package/dist/lib/hotkey.d.ts +9 -0
- package/dist/lib/hotkey.js +101 -0
- package/dist/lib/icon-set.d.ts +95 -0
- package/dist/lib/icon-set.js +97 -0
- package/dist/lib/icons.d.ts +12 -0
- package/dist/lib/icons.js +39 -0
- package/dist/lib/indicator.d.ts +6 -0
- package/dist/lib/indicator.js +18 -0
- package/dist/lib/keep-in-view.d.ts +5 -0
- package/dist/lib/keep-in-view.js +15 -0
- package/dist/lib/key-shortcut.d.ts +20 -0
- package/dist/lib/key-shortcut.js +100 -0
- package/dist/lib/keycaps.d.ts +8 -0
- package/dist/lib/keycaps.js +162 -0
- package/dist/lib/locale.d.ts +4 -0
- package/dist/lib/locale.js +13 -0
- package/dist/lib/nav-tree.d.ts +9 -0
- package/dist/lib/nav-tree.js +30 -0
- package/dist/lib/numeric.d.ts +20 -0
- package/dist/lib/numeric.js +50 -0
- package/dist/lib/option-filter.d.ts +29 -0
- package/dist/lib/option-filter.js +108 -0
- package/dist/lib/option-groups.d.ts +6 -0
- package/dist/lib/option-groups.js +26 -0
- package/dist/lib/options.d.ts +5 -0
- package/dist/lib/options.js +24 -0
- package/dist/lib/pagination.d.ts +9 -0
- package/dist/lib/pagination.js +52 -0
- package/dist/lib/palette.d.ts +8 -0
- package/dist/lib/palette.js +46 -0
- package/dist/lib/progress.d.ts +20 -0
- package/dist/lib/progress.js +33 -0
- package/dist/lib/rating.d.ts +7 -0
- package/dist/lib/rating.js +26 -0
- package/dist/lib/roving-index.d.ts +10 -0
- package/dist/lib/roving-index.js +32 -0
- package/dist/lib/roving.d.ts +11 -0
- package/dist/lib/roving.js +21 -0
- package/dist/lib/slots.d.ts +2 -0
- package/dist/lib/slots.js +6 -0
- package/dist/lib/sparkline.d.ts +11 -0
- package/dist/lib/sparkline.js +42 -0
- package/dist/lib/split.d.ts +12 -0
- package/dist/lib/split.js +37 -0
- package/dist/lib/surface.d.ts +7 -0
- package/dist/lib/surface.js +28 -0
- package/dist/lib/table.d.ts +30 -0
- package/dist/lib/table.js +110 -0
- package/dist/lib/tags.d.ts +14 -0
- package/dist/lib/tags.js +29 -0
- package/dist/lib/text.d.ts +5 -0
- package/dist/lib/text.js +13 -0
- package/dist/lib/tokens.d.ts +5 -0
- package/dist/lib/tokens.js +15 -0
- package/dist/lib/tree.d.ts +16 -0
- package/dist/lib/tree.js +89 -0
- package/dist/lib/trend.d.ts +3 -0
- package/dist/lib/trend.js +15 -0
- package/dist/lib/trigger.d.ts +11 -0
- package/dist/lib/trigger.js +34 -0
- package/dist/molecules/jl-accordion-item.css_raw.js +3 -0
- package/dist/molecules/jl-accordion-item.d.ts +34 -0
- package/dist/molecules/jl-accordion-item.js +82 -0
- package/dist/molecules/jl-accordion.css_raw.js +3 -0
- package/dist/molecules/jl-accordion.d.ts +27 -0
- package/dist/molecules/jl-accordion.js +61 -0
- package/dist/molecules/jl-alert.css_raw.js +3 -0
- package/dist/molecules/jl-alert.d.ts +38 -0
- package/dist/molecules/jl-alert.js +131 -0
- package/dist/molecules/jl-avatar-group.css_raw.js +3 -0
- package/dist/molecules/jl-avatar-group.d.ts +28 -0
- package/dist/molecules/jl-avatar-group.js +69 -0
- package/dist/molecules/jl-breadcrumb.css_raw.js +3 -0
- package/dist/molecules/jl-breadcrumb.d.ts +35 -0
- package/dist/molecules/jl-breadcrumb.js +126 -0
- package/dist/molecules/jl-calendar.css_raw.js +3 -0
- package/dist/molecules/jl-calendar.d.ts +101 -0
- package/dist/molecules/jl-calendar.js +314 -0
- package/dist/molecules/jl-card.css_raw.js +3 -0
- package/dist/molecules/jl-card.d.ts +46 -0
- package/dist/molecules/jl-card.js +172 -0
- package/dist/molecules/jl-code-block.css_raw.js +3 -0
- package/dist/molecules/jl-code-block.d.ts +44 -0
- package/dist/molecules/jl-code-block.js +106 -0
- package/dist/molecules/jl-combobox.css_raw.js +3 -0
- package/dist/molecules/jl-combobox.d.ts +120 -0
- package/dist/molecules/jl-combobox.js +450 -0
- package/dist/molecules/jl-copy-button.css_raw.js +3 -0
- package/dist/molecules/jl-copy-button.d.ts +31 -0
- package/dist/molecules/jl-copy-button.js +69 -0
- package/dist/molecules/jl-date-picker.css_raw.js +3 -0
- package/dist/molecules/jl-date-picker.d.ts +109 -0
- package/dist/molecules/jl-date-picker.js +322 -0
- package/dist/molecules/jl-empty.css_raw.js +3 -0
- package/dist/molecules/jl-empty.d.ts +32 -0
- package/dist/molecules/jl-empty.js +94 -0
- package/dist/molecules/jl-field.css_raw.js +3 -0
- package/dist/molecules/jl-field.d.ts +49 -0
- package/dist/molecules/jl-field.js +147 -0
- package/dist/molecules/jl-file-drop.css_raw.js +3 -0
- package/dist/molecules/jl-file-drop.d.ts +93 -0
- package/dist/molecules/jl-file-drop.js +366 -0
- package/dist/molecules/jl-list-item.css_raw.js +3 -0
- package/dist/molecules/jl-list-item.d.ts +56 -0
- package/dist/molecules/jl-list-item.js +176 -0
- package/dist/molecules/jl-menu.css_raw.js +3 -0
- package/dist/molecules/jl-menu.d.ts +62 -0
- package/dist/molecules/jl-menu.js +288 -0
- package/dist/molecules/jl-number-input.css_raw.js +3 -0
- package/dist/molecules/jl-number-input.d.ts +81 -0
- package/dist/molecules/jl-number-input.js +220 -0
- package/dist/molecules/jl-pagination.css_raw.js +3 -0
- package/dist/molecules/jl-pagination.d.ts +59 -0
- package/dist/molecules/jl-pagination.js +172 -0
- package/dist/molecules/jl-popover.css_raw.js +3 -0
- package/dist/molecules/jl-popover.d.ts +49 -0
- package/dist/molecules/jl-popover.js +152 -0
- package/dist/molecules/jl-radio-group.css_raw.js +3 -0
- package/dist/molecules/jl-radio-group.d.ts +71 -0
- package/dist/molecules/jl-radio-group.js +202 -0
- package/dist/molecules/jl-search.css_raw.js +3 -0
- package/dist/molecules/jl-search.d.ts +71 -0
- package/dist/molecules/jl-search.js +210 -0
- package/dist/molecules/jl-segmented.css_raw.js +3 -0
- package/dist/molecules/jl-segmented.d.ts +65 -0
- package/dist/molecules/jl-segmented.js +187 -0
- package/dist/molecules/jl-stat.css_raw.js +3 -0
- package/dist/molecules/jl-stat.d.ts +55 -0
- package/dist/molecules/jl-stat.js +169 -0
- package/dist/molecules/jl-steps.css_raw.js +3 -0
- package/dist/molecules/jl-steps.d.ts +44 -0
- package/dist/molecules/jl-steps.js +118 -0
- package/dist/molecules/jl-tab-panel.css_raw.js +3 -0
- package/dist/molecules/jl-tab-panel.d.ts +35 -0
- package/dist/molecules/jl-tab-panel.js +35 -0
- package/dist/molecules/jl-tabs.css_raw.js +3 -0
- package/dist/molecules/jl-tabs.d.ts +51 -0
- package/dist/molecules/jl-tabs.js +198 -0
- package/dist/molecules/jl-tag-input.css_raw.js +3 -0
- package/dist/molecules/jl-tag-input.d.ts +103 -0
- package/dist/molecules/jl-tag-input.js +429 -0
- package/dist/molecules/jl-timeline.css_raw.js +3 -0
- package/dist/molecules/jl-timeline.d.ts +28 -0
- package/dist/molecules/jl-timeline.js +81 -0
- package/dist/molecules/jl-toast.css_raw.js +3 -0
- package/dist/molecules/jl-toast.d.ts +75 -0
- package/dist/molecules/jl-toast.js +274 -0
- package/dist/molecules/jl-tooltip.css_raw.js +3 -0
- package/dist/molecules/jl-tooltip.d.ts +44 -0
- package/dist/molecules/jl-tooltip.js +160 -0
- package/dist/organisms/jl-command-palette.css_raw.js +3 -0
- package/dist/organisms/jl-command-palette.d.ts +60 -0
- package/dist/organisms/jl-command-palette.js +285 -0
- package/dist/organisms/jl-confirm.css_raw.js +3 -0
- package/dist/organisms/jl-confirm.d.ts +56 -0
- package/dist/organisms/jl-confirm.js +132 -0
- package/dist/organisms/jl-dialog.css_raw.js +3 -0
- package/dist/organisms/jl-dialog.d.ts +43 -0
- package/dist/organisms/jl-dialog.js +120 -0
- package/dist/organisms/jl-drawer.css_raw.js +3 -0
- package/dist/organisms/jl-drawer.d.ts +50 -0
- package/dist/organisms/jl-drawer.js +141 -0
- package/dist/organisms/jl-navbar.css_raw.js +3 -0
- package/dist/organisms/jl-navbar.d.ts +32 -0
- package/dist/organisms/jl-navbar.js +121 -0
- package/dist/organisms/jl-page-header.css_raw.js +3 -0
- package/dist/organisms/jl-page-header.d.ts +26 -0
- package/dist/organisms/jl-page-header.js +85 -0
- package/dist/organisms/jl-side-nav.css_raw.js +3 -0
- package/dist/organisms/jl-side-nav.d.ts +60 -0
- package/dist/organisms/jl-side-nav.js +269 -0
- package/dist/organisms/jl-table.css_raw.js +3 -0
- package/dist/organisms/jl-table.d.ts +109 -0
- package/dist/organisms/jl-table.js +318 -0
- package/dist/organisms/jl-toaster.css_raw.js +3 -0
- package/dist/organisms/jl-toaster.d.ts +48 -0
- package/dist/organisms/jl-toaster.js +142 -0
- package/dist/organisms/jl-tree.css_raw.js +3 -0
- package/dist/organisms/jl-tree.d.ts +59 -0
- package/dist/organisms/jl-tree.js +198 -0
- package/dist/styles/base.css_raw.js +3 -0
- package/dist/styles/control.css_raw.js +3 -0
- package/dist/styles/floating.css_raw.js +3 -0
- package/dist/styles/shared.d.ts +8 -0
- package/dist/styles/shared.js +14 -0
- package/dist/styles/tokens.css_raw.js +3 -0
- package/dist/templates/jl-app-shell.css_raw.js +3 -0
- package/dist/templates/jl-app-shell.d.ts +37 -0
- package/dist/templates/jl-app-shell.js +150 -0
- package/dist/templates/jl-split-view.css_raw.js +3 -0
- package/dist/templates/jl-split-view.d.ts +49 -0
- package/dist/templates/jl-split-view.js +149 -0
- package/dist/tokens.css +188 -0
- package/dist/types/calendar.types.d.ts +11 -0
- package/dist/types/files.types.d.ts +23 -0
- package/dist/types/floating.types.d.ts +13 -0
- package/dist/types/fuzzy.types.d.ts +10 -0
- package/dist/types/highlight.types.d.ts +6 -0
- package/dist/types/hotkey.types.d.ts +19 -0
- package/dist/types/jl-accordion.types.d.ts +2 -0
- package/dist/types/jl-avatar.types.d.ts +10 -0
- package/dist/types/jl-breadcrumb.types.d.ts +6 -0
- package/dist/types/jl-button.types.d.ts +3 -0
- package/dist/types/jl-card.types.d.ts +2 -0
- package/dist/types/jl-chip.types.d.ts +1 -0
- package/dist/types/jl-combobox.types.d.ts +8 -0
- package/dist/types/jl-command-palette.types.d.ts +22 -0
- package/dist/types/jl-confirm.types.d.ts +10 -0
- package/dist/types/jl-dialog.types.d.ts +1 -0
- package/dist/types/jl-divider.types.d.ts +2 -0
- package/dist/types/jl-drawer.types.d.ts +4 -0
- package/dist/types/jl-empty.types.d.ts +2 -0
- package/dist/types/jl-field.types.d.ts +2 -0
- package/dist/types/jl-input.types.d.ts +1 -0
- package/dist/types/jl-link.types.d.ts +2 -0
- package/dist/types/jl-menu.types.d.ts +22 -0
- package/dist/types/jl-navbar.types.d.ts +6 -0
- package/dist/types/jl-progress.types.d.ts +3 -0
- package/dist/types/jl-radio-group.types.d.ts +10 -0
- package/dist/types/jl-segmented.types.d.ts +7 -0
- package/dist/types/jl-select.types.d.ts +12 -0
- package/dist/types/jl-side-nav.types.d.ts +17 -0
- package/dist/types/jl-skeleton.types.d.ts +2 -0
- package/dist/types/jl-slider.types.d.ts +5 -0
- package/dist/types/jl-split-view.types.d.ts +2 -0
- package/dist/types/jl-stat.types.d.ts +2 -0
- package/dist/types/jl-steps.types.d.ts +8 -0
- package/dist/types/jl-switch.types.d.ts +1 -0
- package/dist/types/jl-table.types.d.ts +30 -0
- package/dist/types/jl-tabs.types.d.ts +2 -0
- package/dist/types/jl-timeline.types.d.ts +10 -0
- package/dist/types/jl-toast.types.d.ts +2 -0
- package/dist/types/jl-toaster.types.d.ts +23 -0
- package/dist/types/jl-tree.types.d.ts +26 -0
- package/dist/types/key-shortcut.types.d.ts +9 -0
- package/dist/types/keycaps.types.d.ts +7 -0
- package/dist/types/option-filter.types.d.ts +10 -0
- package/dist/types/options.types.d.ts +4 -0
- package/dist/types/pagination.types.d.ts +8 -0
- package/dist/types/roving-index.types.d.ts +2 -0
- package/dist/types/sparkline.types.d.ts +11 -0
- package/package.json +102 -4
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// src/lib/floating.ts
|
|
2
|
+
var PLACEMENTS = [
|
|
3
|
+
"top",
|
|
4
|
+
"top-start",
|
|
5
|
+
"top-end",
|
|
6
|
+
"bottom",
|
|
7
|
+
"bottom-start",
|
|
8
|
+
"bottom-end",
|
|
9
|
+
"left",
|
|
10
|
+
"left-start",
|
|
11
|
+
"left-end",
|
|
12
|
+
"right",
|
|
13
|
+
"right-start",
|
|
14
|
+
"right-end"
|
|
15
|
+
];
|
|
16
|
+
var OPPOSITE = {
|
|
17
|
+
bottom: "top",
|
|
18
|
+
left: "right",
|
|
19
|
+
right: "left",
|
|
20
|
+
top: "bottom"
|
|
21
|
+
};
|
|
22
|
+
var VIEWPORT_MARGIN = 8;
|
|
23
|
+
var DEFAULT_GAP = 6;
|
|
24
|
+
var splitPlacement = (placement) => {
|
|
25
|
+
const [side, align] = placement.split("-");
|
|
26
|
+
return {
|
|
27
|
+
align: align ?? "center",
|
|
28
|
+
side
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
var joinPlacement = (side, align) => align === "center" ? side : `${side}-${align}`;
|
|
32
|
+
var isVertical = (side) => side === "top" || side === "bottom";
|
|
33
|
+
/** Where the floating element starts along the anchor's edge: its start, its centre, or its end. */
|
|
34
|
+
function alignedOffset(start, anchorLength, floatingLength, align) {
|
|
35
|
+
return {
|
|
36
|
+
center: start + (anchorLength - floatingLength) / 2,
|
|
37
|
+
end: start + anchorLength - floatingLength,
|
|
38
|
+
start
|
|
39
|
+
}[align];
|
|
40
|
+
}
|
|
41
|
+
function pointFor(anchor, floating, side, align, gap) {
|
|
42
|
+
const left = alignedOffset(anchor.left, anchor.width, floating.width, align);
|
|
43
|
+
const top = alignedOffset(anchor.top, anchor.height, floating.height, align);
|
|
44
|
+
return {
|
|
45
|
+
bottom: {
|
|
46
|
+
left,
|
|
47
|
+
top: anchor.top + anchor.height + gap
|
|
48
|
+
},
|
|
49
|
+
left: {
|
|
50
|
+
left: anchor.left - floating.width - gap,
|
|
51
|
+
top
|
|
52
|
+
},
|
|
53
|
+
right: {
|
|
54
|
+
left: anchor.left + anchor.width + gap,
|
|
55
|
+
top
|
|
56
|
+
},
|
|
57
|
+
top: {
|
|
58
|
+
left,
|
|
59
|
+
top: anchor.top - floating.height - gap
|
|
60
|
+
}
|
|
61
|
+
}[side];
|
|
62
|
+
}
|
|
63
|
+
/** How much of the floating element would leave the window on its main side; 0 when it fits. */
|
|
64
|
+
function overflowOn(point, floating, viewport, side) {
|
|
65
|
+
const overflows = {
|
|
66
|
+
bottom: point.top + floating.height - (viewport.height - VIEWPORT_MARGIN),
|
|
67
|
+
left: VIEWPORT_MARGIN - point.left,
|
|
68
|
+
right: point.left + floating.width - (viewport.width - VIEWPORT_MARGIN),
|
|
69
|
+
top: VIEWPORT_MARGIN - point.top
|
|
70
|
+
};
|
|
71
|
+
return Math.max(0, overflows[side]);
|
|
72
|
+
}
|
|
73
|
+
var clamp = (value, min, max) => Math.min(Math.max(value, min), Math.max(min, max));
|
|
74
|
+
/**
|
|
75
|
+
* Where to put a floating element next to its anchor, in window coordinates: on the asked side when it fits,
|
|
76
|
+
* otherwise on the opposite side if that leaves more of it visible; then kept inside the window.
|
|
77
|
+
*/
|
|
78
|
+
function computePosition(anchor, floating, viewport, placement, gap = DEFAULT_GAP) {
|
|
79
|
+
const { side, align } = splitPlacement(placement);
|
|
80
|
+
const wanted = pointFor(anchor, floating, side, align, gap);
|
|
81
|
+
const flipped = pointFor(anchor, floating, OPPOSITE[side], align, gap);
|
|
82
|
+
const wantedOverflow = overflowOn(wanted, floating, viewport, side);
|
|
83
|
+
const flippedOverflow = overflowOn(flipped, floating, viewport, OPPOSITE[side]);
|
|
84
|
+
const useFlipped = wantedOverflow > 0 && flippedOverflow < wantedOverflow;
|
|
85
|
+
const finalSide = useFlipped ? OPPOSITE[side] : side;
|
|
86
|
+
const point = useFlipped ? flipped : wanted;
|
|
87
|
+
const maxLeft = viewport.width - floating.width - VIEWPORT_MARGIN;
|
|
88
|
+
const maxTop = viewport.height - floating.height - VIEWPORT_MARGIN;
|
|
89
|
+
return {
|
|
90
|
+
left: isVertical(finalSide) ? clamp(point.left, VIEWPORT_MARGIN, maxLeft) : point.left,
|
|
91
|
+
placement: joinPlacement(finalSide, align),
|
|
92
|
+
top: isVertical(finalSide) ? point.top : clamp(point.top, VIEWPORT_MARGIN, maxTop)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** Puts a fixed-position element next to its anchor; returns the placement it got after flipping. */
|
|
96
|
+
function placeFloating(anchor, floating, placement, gap) {
|
|
97
|
+
const rect = anchor.getBoundingClientRect();
|
|
98
|
+
const viewport = {
|
|
99
|
+
height: window.innerHeight,
|
|
100
|
+
width: document.documentElement.clientWidth
|
|
101
|
+
};
|
|
102
|
+
const result = computePosition(rect, {
|
|
103
|
+
height: floating.offsetHeight,
|
|
104
|
+
width: floating.offsetWidth
|
|
105
|
+
}, viewport, placement, gap);
|
|
106
|
+
floating.style.left = `${Math.round(result.left)}px`;
|
|
107
|
+
floating.style.top = `${Math.round(result.top)}px`;
|
|
108
|
+
floating.dataset.placement = result.placement;
|
|
109
|
+
return result.placement;
|
|
110
|
+
}
|
|
111
|
+
/** Keeps a floating element by its anchor while the page scrolls or resizes, until the signal aborts. */
|
|
112
|
+
function followAnchor(anchor, floating, placement, signal, gap) {
|
|
113
|
+
const update = () => {
|
|
114
|
+
placeFloating(anchor, floating, placement, gap);
|
|
115
|
+
};
|
|
116
|
+
window.addEventListener("scroll", update, {
|
|
117
|
+
capture: true,
|
|
118
|
+
passive: true,
|
|
119
|
+
signal
|
|
120
|
+
});
|
|
121
|
+
window.addEventListener("resize", update, {
|
|
122
|
+
passive: true,
|
|
123
|
+
signal
|
|
124
|
+
});
|
|
125
|
+
update();
|
|
126
|
+
}
|
|
127
|
+
export { PLACEMENTS, computePosition, followAnchor, placeFloating };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FuzzyMatch, TextSegment } from '../types/fuzzy.types';
|
|
2
|
+
/**
|
|
3
|
+
* What a matched letter is worth. Letters next to each other and letters that start a word count most, so "ofi"
|
|
4
|
+
* finds "Open file" before "Profile", and "set" puts "Settings" before "Reset".
|
|
5
|
+
*/
|
|
6
|
+
export declare const FUZZY_SCORE: {
|
|
7
|
+
/** A letter right after the previous matched one. */
|
|
8
|
+
readonly contiguous: 6;
|
|
9
|
+
/** Taken off for each letter skipped between two matched ones. */
|
|
10
|
+
readonly gap: 1;
|
|
11
|
+
/** Each matched letter. */
|
|
12
|
+
readonly match: 1;
|
|
13
|
+
/** The text's very first letter, on top of the word start. */
|
|
14
|
+
readonly textStart: 4;
|
|
15
|
+
/** A letter that starts a word: after a space, a dash, a slash…, or a capital in camelCase. */
|
|
16
|
+
readonly wordStart: 8;
|
|
17
|
+
};
|
|
18
|
+
/** Whether the letter at `index` starts a word of the text. */
|
|
19
|
+
export declare function isWordStart(text: string, index: number): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Whether the letters of the query appear in the text in order (spaces ignored, any case), and how well: the
|
|
22
|
+
* best placement by contiguity and word starts. `null` when they do not; an empty query matches with 0.
|
|
23
|
+
*/
|
|
24
|
+
export declare function fuzzyMatch(query: string, text: string): FuzzyMatch | null;
|
|
25
|
+
/** The text cut into runs of matched and unmatched letters, for drawing the matches in `<mark>`. */
|
|
26
|
+
export declare function highlightSegments(text: string, positions: readonly number[]): TextSegment[];
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// src/lib/fuzzy.ts
|
|
2
|
+
/**
|
|
3
|
+
* What a matched letter is worth. Letters next to each other and letters that start a word count most, so "ofi"
|
|
4
|
+
* finds "Open file" before "Profile", and "set" puts "Settings" before "Reset".
|
|
5
|
+
*/
|
|
6
|
+
var FUZZY_SCORE = {
|
|
7
|
+
/** A letter right after the previous matched one. */
|
|
8
|
+
contiguous: 6,
|
|
9
|
+
/** Taken off for each letter skipped between two matched ones. */
|
|
10
|
+
gap: 1,
|
|
11
|
+
/** Each matched letter. */
|
|
12
|
+
match: 1,
|
|
13
|
+
/** The text's very first letter, on top of the word start. */
|
|
14
|
+
textStart: 4,
|
|
15
|
+
/** A letter that starts a word: after a space, a dash, a slash…, or a capital in camelCase. */
|
|
16
|
+
wordStart: 8
|
|
17
|
+
};
|
|
18
|
+
var NO_MATCH = Number.NEGATIVE_INFINITY;
|
|
19
|
+
var NO_INDEX = -1;
|
|
20
|
+
var WHITESPACE = /\s+/g;
|
|
21
|
+
var SEPARATORS = /* @__PURE__ */ new Set([
|
|
22
|
+
" ",
|
|
23
|
+
"-",
|
|
24
|
+
"_",
|
|
25
|
+
"/",
|
|
26
|
+
".",
|
|
27
|
+
":",
|
|
28
|
+
"(",
|
|
29
|
+
"[",
|
|
30
|
+
"\\"
|
|
31
|
+
]);
|
|
32
|
+
var isLower = (char) => char !== char.toUpperCase() && char === char.toLowerCase();
|
|
33
|
+
var isUpper = (char) => char !== char.toLowerCase() && char === char.toUpperCase();
|
|
34
|
+
/** Whether the letter at `index` starts a word of the text. */
|
|
35
|
+
function isWordStart(text, index) {
|
|
36
|
+
if (index === 0) return true;
|
|
37
|
+
const previous = text[index - 1] ?? "";
|
|
38
|
+
const current = text[index] ?? "";
|
|
39
|
+
return SEPARATORS.has(previous) || isLower(previous) && isUpper(current);
|
|
40
|
+
}
|
|
41
|
+
function positionBonus(text, index) {
|
|
42
|
+
const wordStart = isWordStart(text, index) ? FUZZY_SCORE.wordStart : 0;
|
|
43
|
+
const textStart = index === 0 ? FUZZY_SCORE.textStart : 0;
|
|
44
|
+
return FUZZY_SCORE.match + wordStart + textStart;
|
|
45
|
+
}
|
|
46
|
+
function firstRow(letter, haystack, text) {
|
|
47
|
+
const scores = new Float64Array(haystack.length).fill(NO_MATCH);
|
|
48
|
+
for (const [index, char] of haystack.entries()) if (char === letter) scores[index] = positionBonus(text, index);
|
|
49
|
+
return {
|
|
50
|
+
from: new Int32Array(haystack.length).fill(NO_INDEX),
|
|
51
|
+
scores
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The best score of each place for the next letter, knowing the previous row. A gapped step costs `gap` per
|
|
56
|
+
* skipped letter: the best gapped predecessor is kept as a running maximum, so a row costs one pass.
|
|
57
|
+
*/
|
|
58
|
+
function nextRow(previous, letter, haystack, text) {
|
|
59
|
+
const scores = new Float64Array(haystack.length).fill(NO_MATCH);
|
|
60
|
+
const from = new Int32Array(haystack.length).fill(NO_INDEX);
|
|
61
|
+
const gapped = {
|
|
62
|
+
index: NO_INDEX,
|
|
63
|
+
value: NO_MATCH
|
|
64
|
+
};
|
|
65
|
+
for (const [index, char] of haystack.entries()) {
|
|
66
|
+
const candidate = index - 2;
|
|
67
|
+
const candidateValue = candidate >= 0 ? (previous.scores[candidate] ?? NO_MATCH) + FUZZY_SCORE.gap * candidate : NO_MATCH;
|
|
68
|
+
if (candidateValue > gapped.value) {
|
|
69
|
+
gapped.value = candidateValue;
|
|
70
|
+
gapped.index = candidate;
|
|
71
|
+
}
|
|
72
|
+
if (char !== letter) continue;
|
|
73
|
+
const contiguous = index >= 1 ? (previous.scores[index - 1] ?? NO_MATCH) + FUZZY_SCORE.contiguous : NO_MATCH;
|
|
74
|
+
const afterGap = gapped.value - FUZZY_SCORE.gap * (index - 1);
|
|
75
|
+
const best = Math.max(contiguous, afterGap);
|
|
76
|
+
if (best === NO_MATCH) continue;
|
|
77
|
+
scores[index] = best + positionBonus(text, index);
|
|
78
|
+
from[index] = contiguous >= afterGap ? index - 1 : gapped.index;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
from,
|
|
82
|
+
scores
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function bestEnd(row) {
|
|
86
|
+
const best = {
|
|
87
|
+
index: NO_INDEX,
|
|
88
|
+
value: NO_MATCH
|
|
89
|
+
};
|
|
90
|
+
for (const [index, value] of row.scores.entries()) if (value > best.value) {
|
|
91
|
+
best.value = value;
|
|
92
|
+
best.index = index;
|
|
93
|
+
}
|
|
94
|
+
return best.index;
|
|
95
|
+
}
|
|
96
|
+
function backtrack(rows, end) {
|
|
97
|
+
const positions = [];
|
|
98
|
+
const cursor = { index: end };
|
|
99
|
+
for (const row of [...rows].reverse()) {
|
|
100
|
+
positions.unshift(cursor.index);
|
|
101
|
+
cursor.index = row.from[cursor.index] ?? NO_INDEX;
|
|
102
|
+
}
|
|
103
|
+
return positions;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Whether the letters of the query appear in the text in order (spaces ignored, any case), and how well: the
|
|
107
|
+
* best placement by contiguity and word starts. `null` when they do not; an empty query matches with 0.
|
|
108
|
+
*/
|
|
109
|
+
function fuzzyMatch(query, text) {
|
|
110
|
+
const needle = query.replace(WHITESPACE, "").toLowerCase().split("");
|
|
111
|
+
const haystack = text.split("").map((char) => char.toLowerCase());
|
|
112
|
+
const [first, ...rest] = needle;
|
|
113
|
+
if (first === void 0) return {
|
|
114
|
+
positions: [],
|
|
115
|
+
score: 0
|
|
116
|
+
};
|
|
117
|
+
if (needle.length > haystack.length) return null;
|
|
118
|
+
const rows = [firstRow(first, haystack, text)];
|
|
119
|
+
for (const letter of rest) {
|
|
120
|
+
const previous = rows[rows.length - 1];
|
|
121
|
+
rows.push(nextRow(previous, letter, haystack, text));
|
|
122
|
+
}
|
|
123
|
+
const last = rows[rows.length - 1];
|
|
124
|
+
const end = bestEnd(last);
|
|
125
|
+
if (end === NO_INDEX) return null;
|
|
126
|
+
return {
|
|
127
|
+
positions: backtrack(rows, end),
|
|
128
|
+
score: last.scores[end] ?? 0
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/** The text cut into runs of matched and unmatched letters, for drawing the matches in `<mark>`. */
|
|
132
|
+
function highlightSegments(text, positions) {
|
|
133
|
+
const matched = new Set(positions);
|
|
134
|
+
const segments = [];
|
|
135
|
+
for (const [index, char] of text.split("").entries()) {
|
|
136
|
+
const marked = matched.has(index);
|
|
137
|
+
const last = segments[segments.length - 1];
|
|
138
|
+
if (last !== void 0 && last.marked === marked) {
|
|
139
|
+
last.text += char;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
segments.push({
|
|
143
|
+
marked,
|
|
144
|
+
text: char
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return segments;
|
|
148
|
+
}
|
|
149
|
+
export { FUZZY_SCORE, fuzzyMatch, highlightSegments, isWordStart };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { CodeLanguage, Token } from '../types/highlight.types';
|
|
2
|
+
/** Splits source code into coloured tokens. Small and forgiving: it draws code, it does not parse it. */
|
|
3
|
+
export declare function tokenize(code: string, language: CodeLanguage): Token[];
|
|
4
|
+
/** The tokens as spans with a `tok-<kind>` class each; plain text stays a text node. */
|
|
5
|
+
export declare function highlightedNodes(code: string, language: CodeLanguage): Node[];
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// src/lib/highlight.ts
|
|
2
|
+
var TS_KEYWORDS = [
|
|
3
|
+
"as",
|
|
4
|
+
"async",
|
|
5
|
+
"await",
|
|
6
|
+
"break",
|
|
7
|
+
"case",
|
|
8
|
+
"catch",
|
|
9
|
+
"class",
|
|
10
|
+
"const",
|
|
11
|
+
"continue",
|
|
12
|
+
"default",
|
|
13
|
+
"delete",
|
|
14
|
+
"do",
|
|
15
|
+
"else",
|
|
16
|
+
"export",
|
|
17
|
+
"extends",
|
|
18
|
+
"false",
|
|
19
|
+
"finally",
|
|
20
|
+
"for",
|
|
21
|
+
"from",
|
|
22
|
+
"function",
|
|
23
|
+
"if",
|
|
24
|
+
"implements",
|
|
25
|
+
"import",
|
|
26
|
+
"in",
|
|
27
|
+
"instanceof",
|
|
28
|
+
"interface",
|
|
29
|
+
"let",
|
|
30
|
+
"new",
|
|
31
|
+
"null",
|
|
32
|
+
"of",
|
|
33
|
+
"private",
|
|
34
|
+
"protected",
|
|
35
|
+
"public",
|
|
36
|
+
"readonly",
|
|
37
|
+
"return",
|
|
38
|
+
"static",
|
|
39
|
+
"super",
|
|
40
|
+
"switch",
|
|
41
|
+
"this",
|
|
42
|
+
"throw",
|
|
43
|
+
"true",
|
|
44
|
+
"try",
|
|
45
|
+
"type",
|
|
46
|
+
"typeof",
|
|
47
|
+
"undefined",
|
|
48
|
+
"var",
|
|
49
|
+
"void",
|
|
50
|
+
"while"
|
|
51
|
+
];
|
|
52
|
+
var sticky = (source) => new RegExp(source, "y");
|
|
53
|
+
var QUOTED_STRING = String.raw`'(?:\\.|[^'\\\n])*'|"(?:\\.|[^"\\\n])*"`;
|
|
54
|
+
var TEMPLATE_STRING = String.raw`\`(?:\\.|[^\`\\])*\``;
|
|
55
|
+
var TS_RULES = [
|
|
56
|
+
{
|
|
57
|
+
kind: "comment",
|
|
58
|
+
pattern: sticky(String.raw`\/\/[^\n]*|\/\*[\s\S]*?\*\/`)
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
kind: "string",
|
|
62
|
+
pattern: sticky(`${QUOTED_STRING}|${TEMPLATE_STRING}`)
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
kind: "tag",
|
|
66
|
+
pattern: sticky(String.raw`<\/?[A-Za-z][\w.-]*|\/?>`)
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
kind: "keyword",
|
|
70
|
+
pattern: sticky(String.raw`\b(?:${TS_KEYWORDS.join("|")})\b`)
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
kind: "number",
|
|
74
|
+
pattern: sticky(String.raw`\b\d[\d_]*(?:\.\d+)?\b`)
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
kind: "function",
|
|
78
|
+
pattern: sticky(String.raw`[A-Za-z_$][\w$]*(?=\s*\()`)
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
kind: "attribute",
|
|
82
|
+
pattern: sticky(String.raw`[A-Za-z_$][\w$-]*(?==[{"'])`)
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
kind: "plain",
|
|
86
|
+
pattern: sticky(String.raw`[A-Za-z_$][\w$]*`)
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
kind: "punctuation",
|
|
90
|
+
pattern: sticky(String.raw`[{}()[\];,.:=<>+\-*/!?&|]`)
|
|
91
|
+
}
|
|
92
|
+
];
|
|
93
|
+
var CSS_RULES = [
|
|
94
|
+
{
|
|
95
|
+
kind: "comment",
|
|
96
|
+
pattern: sticky(String.raw`\/\*[\s\S]*?\*\/`)
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
kind: "string",
|
|
100
|
+
pattern: sticky(String.raw`'[^'\n]*'|"[^"\n]*"`)
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
kind: "keyword",
|
|
104
|
+
pattern: sticky(String.raw`@[\w-]+|!important`)
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
kind: "property",
|
|
108
|
+
pattern: sticky(String.raw`--[\w-]+|[a-z-]+(?=\s*:[^:])`)
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
kind: "number",
|
|
112
|
+
pattern: sticky(String.raw`#[\da-fA-F]{3,8}\b|-?\d*\.?\d+(?:px|rem|em|%|ms|s|vh|vw|dvh|fr|deg|turn)?\b`)
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
kind: "function",
|
|
116
|
+
pattern: sticky(String.raw`[\w-]+(?=\()`)
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
kind: "tag",
|
|
120
|
+
pattern: sticky(String.raw`:{1,2}[\w-]+|[.#][\w-]+|\b[a-z][\w-]*(?=[^{};]*\{)`)
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
kind: "punctuation",
|
|
124
|
+
pattern: sticky(String.raw`[{}();:,>+~*[\]=]`)
|
|
125
|
+
}
|
|
126
|
+
];
|
|
127
|
+
var BASH_RULES = [
|
|
128
|
+
{
|
|
129
|
+
kind: "comment",
|
|
130
|
+
pattern: sticky(String.raw`#[^\n]*`)
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
kind: "string",
|
|
134
|
+
pattern: sticky(String.raw`'[^']*'|"(?:\\.|[^"\\])*"`)
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
kind: "attribute",
|
|
138
|
+
pattern: sticky(String.raw`(?<=\s)--?[\w-]+`)
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
kind: "function",
|
|
142
|
+
pattern: sticky(String.raw`(?<=^|\n|&&\s|\|\s)[\w.-]+`)
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
kind: "plain",
|
|
146
|
+
pattern: sticky(String.raw`[^\s'"#]+`)
|
|
147
|
+
}
|
|
148
|
+
];
|
|
149
|
+
var HTML_RULES = [{
|
|
150
|
+
kind: "comment",
|
|
151
|
+
pattern: sticky(String.raw`<!--[\s\S]*?-->`)
|
|
152
|
+
}, {
|
|
153
|
+
kind: "tag",
|
|
154
|
+
pattern: sticky(String.raw`<\/?[A-Za-z][\w-]*`)
|
|
155
|
+
}];
|
|
156
|
+
var HTML_TAG_RULES = [
|
|
157
|
+
{
|
|
158
|
+
kind: "string",
|
|
159
|
+
pattern: sticky(`"[^"]*"|'[^']*'`)
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
kind: "attribute",
|
|
163
|
+
pattern: sticky(String.raw`[^\s=>/"']+`)
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
kind: "punctuation",
|
|
167
|
+
pattern: sticky("=")
|
|
168
|
+
}
|
|
169
|
+
];
|
|
170
|
+
var SPACE = {
|
|
171
|
+
kind: "plain",
|
|
172
|
+
pattern: sticky(String.raw`\s+`)
|
|
173
|
+
};
|
|
174
|
+
var TAG_END = {
|
|
175
|
+
kind: "tag",
|
|
176
|
+
pattern: sticky(String.raw`\/?>`)
|
|
177
|
+
};
|
|
178
|
+
function matchAt(rules, code, position) {
|
|
179
|
+
for (const rule of rules) {
|
|
180
|
+
rule.pattern.lastIndex = position;
|
|
181
|
+
const match = rule.pattern.exec(code);
|
|
182
|
+
if (match !== null && match[0] !== "") return {
|
|
183
|
+
kind: rule.kind,
|
|
184
|
+
text: match[0]
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
/** What no rule matches is plain text, one character at a time. */
|
|
190
|
+
var nextToken = (rules, code, position) => matchAt(rules, code, position) ?? {
|
|
191
|
+
kind: "plain",
|
|
192
|
+
text: code.charAt(position)
|
|
193
|
+
};
|
|
194
|
+
/** Joins neighbouring tokens of the same kind, so the result has as few spans as it can. */
|
|
195
|
+
function merge(tokens) {
|
|
196
|
+
return tokens.reduce((merged, token) => {
|
|
197
|
+
const last = merged.at(-1);
|
|
198
|
+
if (last !== void 0 && last.kind === token.kind) {
|
|
199
|
+
last.text += token.text;
|
|
200
|
+
return merged;
|
|
201
|
+
}
|
|
202
|
+
merged.push({ ...token });
|
|
203
|
+
return merged;
|
|
204
|
+
}, []);
|
|
205
|
+
}
|
|
206
|
+
/** Splits code with a flat list of rules. */
|
|
207
|
+
function scan(code, rules) {
|
|
208
|
+
const withSpace = [SPACE, ...rules];
|
|
209
|
+
const tokens = [];
|
|
210
|
+
let position = 0;
|
|
211
|
+
while (position < code.length) {
|
|
212
|
+
const token = nextToken(withSpace, code, position);
|
|
213
|
+
tokens.push(token);
|
|
214
|
+
position += token.text.length;
|
|
215
|
+
}
|
|
216
|
+
return merge(tokens);
|
|
217
|
+
}
|
|
218
|
+
var OUTSIDE_TAG = [SPACE, ...HTML_RULES];
|
|
219
|
+
var INSIDE_TAG = [
|
|
220
|
+
TAG_END,
|
|
221
|
+
SPACE,
|
|
222
|
+
...HTML_TAG_RULES
|
|
223
|
+
];
|
|
224
|
+
/** HTML needs two states: outside a tag (text, comments, tag names) and inside one (attributes). */
|
|
225
|
+
function scanHtml(code) {
|
|
226
|
+
const tokens = [];
|
|
227
|
+
let position = 0;
|
|
228
|
+
let insideTag = false;
|
|
229
|
+
while (position < code.length) {
|
|
230
|
+
const token = nextToken(insideTag ? INSIDE_TAG : OUTSIDE_TAG, code, position);
|
|
231
|
+
insideTag = isInsideTagAfter(token, insideTag);
|
|
232
|
+
tokens.push(token);
|
|
233
|
+
position += token.text.length;
|
|
234
|
+
}
|
|
235
|
+
return merge(tokens);
|
|
236
|
+
}
|
|
237
|
+
/** A tag name opens a tag, `>` or `/>` closes it; any other token leaves the state as it was. */
|
|
238
|
+
function isInsideTagAfter(token, insideTag) {
|
|
239
|
+
if (token.kind !== "tag") return insideTag;
|
|
240
|
+
return token.text.startsWith("<");
|
|
241
|
+
}
|
|
242
|
+
var SCANNERS = {
|
|
243
|
+
bash: (code) => scan(code, BASH_RULES),
|
|
244
|
+
css: (code) => scan(code, CSS_RULES),
|
|
245
|
+
html: scanHtml,
|
|
246
|
+
text: (code) => [{
|
|
247
|
+
kind: "plain",
|
|
248
|
+
text: code
|
|
249
|
+
}],
|
|
250
|
+
ts: (code) => scan(code, TS_RULES)
|
|
251
|
+
};
|
|
252
|
+
/** Splits source code into coloured tokens. Small and forgiving: it draws code, it does not parse it. */
|
|
253
|
+
function tokenize(code, language) {
|
|
254
|
+
if (code === "") return [];
|
|
255
|
+
return SCANNERS[language](code);
|
|
256
|
+
}
|
|
257
|
+
/** The tokens as spans with a `tok-<kind>` class each; plain text stays a text node. */
|
|
258
|
+
function highlightedNodes(code, language) {
|
|
259
|
+
return tokenize(code, language).map((token) => {
|
|
260
|
+
if (token.kind === "plain") return document.createTextNode(token.text);
|
|
261
|
+
const span = document.createElement("span");
|
|
262
|
+
span.className = `tok-${token.kind}`;
|
|
263
|
+
span.textContent = token.text;
|
|
264
|
+
return span;
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
export { highlightedNodes, tokenize };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Hotkey, KeyPress } from '../types/hotkey.types';
|
|
2
|
+
/** Reads a combination such as `mod+k` or `ctrl+shift+p`: `mod` is ⌘ on Apple and Ctrl elsewhere. */
|
|
3
|
+
export declare function parseHotkey(text: string): Hotkey;
|
|
4
|
+
/** Whether a key press is the combination; extra modifiers make it another one. */
|
|
5
|
+
export declare function matchesHotkey(press: KeyPress, hotkey: Hotkey, apple: boolean): boolean;
|
|
6
|
+
/** The keycaps of a combination, modifiers first: `['⌘', 'K']` on a Mac, `['Ctrl', 'K']` elsewhere. */
|
|
7
|
+
export declare function hotkeyLabels(text: string, apple: boolean): string[];
|
|
8
|
+
/** Whether the keyboard has ⌘: Macs, iPhones and iPads. */
|
|
9
|
+
export declare function isApplePlatform(platform: string): boolean;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// src/lib/hotkey.ts
|
|
2
|
+
var SEPARATOR = "+";
|
|
3
|
+
var MODIFIER_NAMES = {
|
|
4
|
+
alt: "alt",
|
|
5
|
+
cmd: "meta",
|
|
6
|
+
command: "meta",
|
|
7
|
+
control: "ctrl",
|
|
8
|
+
ctrl: "ctrl",
|
|
9
|
+
meta: "meta",
|
|
10
|
+
mod: "mod",
|
|
11
|
+
option: "alt",
|
|
12
|
+
shift: "shift"
|
|
13
|
+
};
|
|
14
|
+
/** How each modifier reads on a keycap, on Apple keyboards and elsewhere. */
|
|
15
|
+
var MODIFIER_LABELS = {
|
|
16
|
+
alt: {
|
|
17
|
+
apple: "⌥",
|
|
18
|
+
other: "Alt"
|
|
19
|
+
},
|
|
20
|
+
ctrl: {
|
|
21
|
+
apple: "⌃",
|
|
22
|
+
other: "Ctrl"
|
|
23
|
+
},
|
|
24
|
+
meta: {
|
|
25
|
+
apple: "⌘",
|
|
26
|
+
other: "Win"
|
|
27
|
+
},
|
|
28
|
+
mod: {
|
|
29
|
+
apple: "⌘",
|
|
30
|
+
other: "Ctrl"
|
|
31
|
+
},
|
|
32
|
+
shift: {
|
|
33
|
+
apple: "⇧",
|
|
34
|
+
other: "Shift"
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
/** The order keycaps are written in: ⌃⌥⇧⌘ on Apple, Ctrl+Alt+Shift elsewhere. */
|
|
38
|
+
var MODIFIER_ORDER = {
|
|
39
|
+
apple: [
|
|
40
|
+
"ctrl",
|
|
41
|
+
"alt",
|
|
42
|
+
"shift",
|
|
43
|
+
"mod",
|
|
44
|
+
"meta"
|
|
45
|
+
],
|
|
46
|
+
other: [
|
|
47
|
+
"mod",
|
|
48
|
+
"ctrl",
|
|
49
|
+
"alt",
|
|
50
|
+
"shift",
|
|
51
|
+
"meta"
|
|
52
|
+
]
|
|
53
|
+
};
|
|
54
|
+
var KEY_LABELS = {
|
|
55
|
+
arrowdown: "↓",
|
|
56
|
+
arrowleft: "←",
|
|
57
|
+
arrowright: "→",
|
|
58
|
+
arrowup: "↑",
|
|
59
|
+
backspace: "⌫",
|
|
60
|
+
enter: "↵",
|
|
61
|
+
esc: "Esc",
|
|
62
|
+
escape: "Esc",
|
|
63
|
+
space: "Space",
|
|
64
|
+
tab: "Tab"
|
|
65
|
+
};
|
|
66
|
+
/** Reads a combination such as `mod+k` or `ctrl+shift+p`: `mod` is ⌘ on Apple and Ctrl elsewhere. */
|
|
67
|
+
function parseHotkey(text) {
|
|
68
|
+
const parts = text.toLowerCase().split(SEPARATOR).map((part) => part.trim()).filter((part) => part !== "");
|
|
69
|
+
const modifiers = new Set(parts.map((part) => MODIFIER_NAMES[part]).filter((name) => name !== void 0));
|
|
70
|
+
const key = parts.filter((part) => MODIFIER_NAMES[part] === void 0).at(-1) ?? "";
|
|
71
|
+
return {
|
|
72
|
+
alt: modifiers.has("alt"),
|
|
73
|
+
ctrl: modifiers.has("ctrl"),
|
|
74
|
+
key,
|
|
75
|
+
meta: modifiers.has("meta"),
|
|
76
|
+
mod: modifiers.has("mod"),
|
|
77
|
+
shift: modifiers.has("shift")
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** Whether a key press is the combination; extra modifiers make it another one. */
|
|
81
|
+
function matchesHotkey(press, hotkey, apple) {
|
|
82
|
+
if (hotkey.key === "" || press.key.toLowerCase() !== hotkey.key) return false;
|
|
83
|
+
const wantsCtrl = hotkey.ctrl || hotkey.mod && !apple;
|
|
84
|
+
const wantsMeta = hotkey.meta || hotkey.mod && apple;
|
|
85
|
+
return press.ctrlKey === wantsCtrl && press.metaKey === wantsMeta && press.shiftKey === hotkey.shift && press.altKey === hotkey.alt;
|
|
86
|
+
}
|
|
87
|
+
function keyLabel(key) {
|
|
88
|
+
return KEY_LABELS[key] ?? (key.length === 1 ? key.toUpperCase() : key.charAt(0).toUpperCase() + key.slice(1));
|
|
89
|
+
}
|
|
90
|
+
/** The keycaps of a combination, modifiers first: `['⌘', 'K']` on a Mac, `['Ctrl', 'K']` elsewhere. */
|
|
91
|
+
function hotkeyLabels(text, apple) {
|
|
92
|
+
const hotkey = parseHotkey(text);
|
|
93
|
+
const platform = apple ? "apple" : "other";
|
|
94
|
+
const modifiers = MODIFIER_ORDER[platform].filter((modifier) => hotkey[modifier]).map((modifier) => MODIFIER_LABELS[modifier][platform]);
|
|
95
|
+
return hotkey.key === "" ? modifiers : [...modifiers, keyLabel(hotkey.key)];
|
|
96
|
+
}
|
|
97
|
+
/** Whether the keyboard has ⌘: Macs, iPhones and iPads. */
|
|
98
|
+
function isApplePlatform(platform) {
|
|
99
|
+
return /mac|iphone|ipad|ipod/i.test(platform);
|
|
100
|
+
}
|
|
101
|
+
export { hotkeyLabels, isApplePlatform, matchesHotkey, parseHotkey };
|