@mvarble/mesearch-ui 0.0.0-stage → 0.1.0
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 +84 -2
- package/dist/ambient.d.ts +2 -0
- package/dist/components/Article.svelte +226 -0
- package/dist/components/Article.svelte.d.ts +16 -0
- package/dist/components/ArticleHeader.svelte +134 -0
- package/dist/components/ArticleHeader.svelte.d.ts +19 -0
- package/dist/components/Chips.svelte +22 -0
- package/dist/components/Chips.svelte.d.ts +8 -0
- package/dist/components/Graph.svelte +727 -0
- package/dist/components/Graph.svelte.d.ts +16 -0
- package/dist/components/GraphPanel.svelte +328 -0
- package/dist/components/GraphPanel.svelte.d.ts +17 -0
- package/dist/components/Icon.svelte +67 -0
- package/dist/components/Icon.svelte.d.ts +8 -0
- package/dist/components/Index.svelte +283 -0
- package/dist/components/Index.svelte.d.ts +9 -0
- package/dist/components/Pager.svelte +71 -0
- package/dist/components/Pager.svelte.d.ts +12 -0
- package/dist/components/Palette.svelte +333 -0
- package/dist/components/Palette.svelte.d.ts +9 -0
- package/dist/components/Parts.svelte +62 -0
- package/dist/components/Parts.svelte.d.ts +11 -0
- package/dist/components/Proof.svelte +39 -0
- package/dist/components/Proof.svelte.d.ts +8 -0
- package/dist/components/Rail.svelte +186 -0
- package/dist/components/Rail.svelte.d.ts +13 -0
- package/dist/components/References.svelte +94 -0
- package/dist/components/References.svelte.d.ts +8 -0
- package/dist/components/SequenceTrack.svelte +186 -0
- package/dist/components/SequenceTrack.svelte.d.ts +11 -0
- package/dist/components/Shell.svelte +82 -0
- package/dist/components/Shell.svelte.d.ts +14 -0
- package/dist/components/Statement.svelte +105 -0
- package/dist/components/Statement.svelte.d.ts +11 -0
- package/dist/components/ThemeToggle.svelte +19 -0
- package/dist/components/ThemeToggle.svelte.d.ts +3 -0
- package/dist/components/Toc.svelte +145 -0
- package/dist/components/Toc.svelte.d.ts +8 -0
- package/dist/fonts.d.ts +4 -0
- package/dist/fonts.js +6 -0
- package/dist/fuzzy.d.ts +12 -0
- package/dist/fuzzy.js +57 -0
- package/dist/graph-layout.d.ts +35 -0
- package/dist/graph-layout.js +98 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +30 -0
- package/dist/reading.svelte.d.ts +3 -0
- package/dist/reading.svelte.js +4 -0
- package/dist/server.d.ts +26 -0
- package/dist/server.js +115 -0
- package/dist/settings.d.ts +10 -0
- package/dist/settings.js +28 -0
- package/dist/styles/base.css +136 -0
- package/dist/styles/index.css +6 -0
- package/dist/styles/prose.css +203 -0
- package/dist/styles/tokens.css +111 -0
- package/dist/theme.svelte.d.ts +12 -0
- package/dist/theme.svelte.js +66 -0
- package/dist/types.d.ts +84 -0
- package/dist/types.js +3 -0
- package/package.json +66 -3
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, type Snippet } from 'svelte';
|
|
3
|
+
import { reading } from '../reading.svelte.js';
|
|
4
|
+
import { theme } from '../theme.svelte.js';
|
|
5
|
+
import type { RailLink, SearchEntry } from '../types.js';
|
|
6
|
+
import Palette from './Palette.svelte';
|
|
7
|
+
import Rail from './Rail.svelte';
|
|
8
|
+
|
|
9
|
+
// What every page sits in: the rail (a dock on phones), the search
|
|
10
|
+
// palette, a skip link, and the theme kept applied. Wrap a root layout's
|
|
11
|
+
// children in it.
|
|
12
|
+
let {
|
|
13
|
+
title,
|
|
14
|
+
home,
|
|
15
|
+
links = [],
|
|
16
|
+
search,
|
|
17
|
+
navigate,
|
|
18
|
+
logo,
|
|
19
|
+
children,
|
|
20
|
+
}: {
|
|
21
|
+
title: string;
|
|
22
|
+
home: string;
|
|
23
|
+
links?: RailLink[];
|
|
24
|
+
// Where the palette's entries come from; without it there is no search.
|
|
25
|
+
search?: string | (() => Promise<SearchEntry[]>);
|
|
26
|
+
navigate?: (url: string) => unknown;
|
|
27
|
+
// The rail's mark, instead of the title's initials.
|
|
28
|
+
logo?: Snippet;
|
|
29
|
+
children: Snippet;
|
|
30
|
+
} = $props();
|
|
31
|
+
|
|
32
|
+
let searching = $state(false);
|
|
33
|
+
|
|
34
|
+
onMount(() => theme.start());
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<a class="skip" href="#main">Skip to content</a>
|
|
38
|
+
<Rail
|
|
39
|
+
{title}
|
|
40
|
+
{home}
|
|
41
|
+
{links}
|
|
42
|
+
onsearch={search ? () => (searching = true) : undefined}
|
|
43
|
+
progress={reading.progress}
|
|
44
|
+
{logo}
|
|
45
|
+
/>
|
|
46
|
+
{#if search}
|
|
47
|
+
<Palette index={search} {navigate} bind:open={searching} />
|
|
48
|
+
{/if}
|
|
49
|
+
|
|
50
|
+
<div class="shell">
|
|
51
|
+
{@render children()}
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<style>
|
|
55
|
+
.shell {
|
|
56
|
+
min-height: 100vh;
|
|
57
|
+
padding-bottom: 6rem;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@media (min-width: 960px) {
|
|
61
|
+
.shell {
|
|
62
|
+
margin-left: var(--rail-width);
|
|
63
|
+
padding-bottom: 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.skip {
|
|
68
|
+
position: absolute;
|
|
69
|
+
left: 1rem;
|
|
70
|
+
top: -3rem;
|
|
71
|
+
z-index: 50;
|
|
72
|
+
padding: 0.5rem 0.9rem;
|
|
73
|
+
background: var(--accent);
|
|
74
|
+
color: var(--accent-ink);
|
|
75
|
+
border-radius: var(--radius);
|
|
76
|
+
font: 500 var(--font-size-ui) var(--font-ui);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.skip:focus {
|
|
80
|
+
top: 1rem;
|
|
81
|
+
}
|
|
82
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
import type { RailLink, SearchEntry } from '../types.js';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
title: string;
|
|
5
|
+
home: string;
|
|
6
|
+
links?: RailLink[];
|
|
7
|
+
search?: string | (() => Promise<SearchEntry[]>);
|
|
8
|
+
navigate?: (url: string) => unknown;
|
|
9
|
+
logo?: Snippet;
|
|
10
|
+
children: Snippet;
|
|
11
|
+
};
|
|
12
|
+
declare const Shell: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
13
|
+
type Shell = ReturnType<typeof Shell>;
|
|
14
|
+
export default Shell;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Component } from 'svelte';
|
|
3
|
+
import type { StatementInfo } from '../types.js';
|
|
4
|
+
|
|
5
|
+
// A theorem, a definition, a remark: a document of its own, spread in as
|
|
6
|
+
// `<Statement {...theorem} />`. Its heading runs into its first line, and
|
|
7
|
+
// claims --- theorems, lemmas and their kin --- are set in italics, as in
|
|
8
|
+
// a textbook; definitions, remarks and examples stay upright.
|
|
9
|
+
let {
|
|
10
|
+
default: Body,
|
|
11
|
+
cms,
|
|
12
|
+
noLabel = false,
|
|
13
|
+
noBlock = false,
|
|
14
|
+
}: {
|
|
15
|
+
default: Component;
|
|
16
|
+
cms: StatementInfo;
|
|
17
|
+
// Leave out the "Theorem 2." heading.
|
|
18
|
+
noLabel?: boolean;
|
|
19
|
+
// Render it without its frame, as part of the surrounding text.
|
|
20
|
+
noBlock?: boolean;
|
|
21
|
+
} = $props();
|
|
22
|
+
|
|
23
|
+
const UPRIGHT = /^(definition|remark|example|notation|note|exercise|assumption|problem)s?$/i;
|
|
24
|
+
|
|
25
|
+
let upright = $derived(UPRIGHT.test(cms.kind.trim()));
|
|
26
|
+
let name = $derived(
|
|
27
|
+
cms.kind
|
|
28
|
+
.split(' ')
|
|
29
|
+
.map((word) => word.slice(0, 1).toUpperCase() + word.slice(1))
|
|
30
|
+
.join(' '),
|
|
31
|
+
);
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<div class="statement" class:upright class:bare={noBlock} id={cms.id ?? cms.slug}>
|
|
35
|
+
{#if !noLabel}
|
|
36
|
+
<span class="heading"
|
|
37
|
+
>{name} {cms.label}{#if cms.title}<span class="title">({cms.title})</span
|
|
38
|
+
>{/if}.</span
|
|
39
|
+
>
|
|
40
|
+
{/if}<Body />
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<style>
|
|
44
|
+
.statement {
|
|
45
|
+
margin: 1.75em 0;
|
|
46
|
+
padding: 0.1em 0 0.1em 1.1em;
|
|
47
|
+
border-left: 2px solid color-mix(in srgb, var(--accent) 45%, transparent);
|
|
48
|
+
font-style: italic;
|
|
49
|
+
scroll-margin-top: 2rem;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* Equation numbers stay upright, as they are everywhere else. */
|
|
53
|
+
.statement :global(.math-tag) {
|
|
54
|
+
font-style: normal;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.statement.upright {
|
|
58
|
+
border-left-color: var(--rule-strong);
|
|
59
|
+
font-style: normal;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.statement.bare {
|
|
63
|
+
margin: 0;
|
|
64
|
+
padding: 0;
|
|
65
|
+
border: none;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* A block of its own within the line, so justification leaves its
|
|
69
|
+
* spaces alone. */
|
|
70
|
+
.heading {
|
|
71
|
+
display: inline-block;
|
|
72
|
+
margin-right: 0.3em;
|
|
73
|
+
font-style: normal;
|
|
74
|
+
font-weight: 650;
|
|
75
|
+
color: var(--ink);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.title {
|
|
79
|
+
margin-left: 0.3em;
|
|
80
|
+
font-weight: 400;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* A first paragraph runs on from the heading, as in a textbook. */
|
|
84
|
+
.heading + :global(p) {
|
|
85
|
+
display: inline;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.statement > :global(:first-child:not(.heading)) {
|
|
89
|
+
margin-top: 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.statement > :global(:last-child) {
|
|
93
|
+
margin-bottom: 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.statement:target {
|
|
97
|
+
animation: arrive 2.4s ease-out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@keyframes arrive {
|
|
101
|
+
from {
|
|
102
|
+
background: var(--accent-soft);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
2
|
+
import type { StatementInfo } from '../types.js';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
default: Component;
|
|
5
|
+
cms: StatementInfo;
|
|
6
|
+
noLabel?: boolean;
|
|
7
|
+
noBlock?: boolean;
|
|
8
|
+
};
|
|
9
|
+
declare const Statement: Component<$$ComponentProps, {}, "">;
|
|
10
|
+
type Statement = ReturnType<typeof Statement>;
|
|
11
|
+
export default Statement;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { theme } from '../theme.svelte.js';
|
|
3
|
+
import Icon from './Icon.svelte';
|
|
4
|
+
|
|
5
|
+
// Shows the theme in use; a click switches to the other one.
|
|
6
|
+
let label = $derived(
|
|
7
|
+
theme.resolved == 'dark' ? 'Dark theme: switch to light' : 'Light theme: switch to dark',
|
|
8
|
+
);
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<button
|
|
12
|
+
class="rail-button needs-js"
|
|
13
|
+
type="button"
|
|
14
|
+
onclick={() => theme.toggle()}
|
|
15
|
+
aria-label={label}
|
|
16
|
+
title={label}
|
|
17
|
+
>
|
|
18
|
+
<Icon name={theme.resolved == 'dark' ? 'moon' : 'sun'} />
|
|
19
|
+
</button>
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import type { TocEntry } from '../types.js';
|
|
4
|
+
|
|
5
|
+
// The document's headings, with the one being read marked. Without
|
|
6
|
+
// JavaScript it is a plain list of anchors.
|
|
7
|
+
let { entries, label = 'On this page' }: { entries: TocEntry[]; label?: string } = $props();
|
|
8
|
+
|
|
9
|
+
let active = $state<string | null>(null);
|
|
10
|
+
// How far down the list the marker sits, for the line beside it.
|
|
11
|
+
let list: HTMLElement | undefined = $state();
|
|
12
|
+
let marker = $state<{ top: number; height: number } | null>(null);
|
|
13
|
+
|
|
14
|
+
const flatten = (items: TocEntry[]): TocEntry[] =>
|
|
15
|
+
items.flatMap((item) => [item, ...flatten(item.children)]);
|
|
16
|
+
|
|
17
|
+
onMount(() => {
|
|
18
|
+
const slugs = flatten(entries).map((entry) => entry.slug);
|
|
19
|
+
const headings = slugs
|
|
20
|
+
.map((slug) => document.getElementById(slug))
|
|
21
|
+
.filter((heading): heading is HTMLElement => !!heading);
|
|
22
|
+
if (!headings.length) return;
|
|
23
|
+
|
|
24
|
+
// The current section is the last heading to have passed a line a
|
|
25
|
+
// little below the top of the screen; before the first, none is.
|
|
26
|
+
let frame = 0;
|
|
27
|
+
const update = () => {
|
|
28
|
+
frame = 0;
|
|
29
|
+
const line = Math.min(140, innerHeight * 0.25);
|
|
30
|
+
let current: string | null = null;
|
|
31
|
+
for (const heading of headings) {
|
|
32
|
+
if (heading.getBoundingClientRect().top - line > 0) break;
|
|
33
|
+
current = heading.id;
|
|
34
|
+
}
|
|
35
|
+
const atEnd = innerHeight + scrollY >= document.documentElement.scrollHeight - 4;
|
|
36
|
+
active = atEnd ? headings.at(-1)!.id : current;
|
|
37
|
+
};
|
|
38
|
+
const schedule = () => (frame ||= requestAnimationFrame(update));
|
|
39
|
+
update();
|
|
40
|
+
addEventListener('scroll', schedule, { passive: true });
|
|
41
|
+
addEventListener('resize', schedule);
|
|
42
|
+
return () => {
|
|
43
|
+
removeEventListener('scroll', schedule);
|
|
44
|
+
removeEventListener('resize', schedule);
|
|
45
|
+
cancelAnimationFrame(frame);
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
$effect(() => {
|
|
50
|
+
const link = active
|
|
51
|
+
? list?.querySelector<HTMLElement>(`a[href="#${CSS.escape(active)}"]`)
|
|
52
|
+
: null;
|
|
53
|
+
marker = link && list ? { top: link.offsetTop, height: link.offsetHeight } : null;
|
|
54
|
+
});
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
{#snippet tree(items: TocEntry[])}
|
|
58
|
+
<ul>
|
|
59
|
+
{#each items as item (item.slug)}
|
|
60
|
+
<li class="depth-{item.depth}">
|
|
61
|
+
<a
|
|
62
|
+
href="#{item.slug}"
|
|
63
|
+
class:active={active == item.slug}
|
|
64
|
+
aria-current={active == item.slug ? 'location' : undefined}
|
|
65
|
+
>
|
|
66
|
+
{@html item.titleHtml}
|
|
67
|
+
</a>
|
|
68
|
+
{#if item.children.length}{@render tree(item.children)}{/if}
|
|
69
|
+
</li>
|
|
70
|
+
{/each}
|
|
71
|
+
</ul>
|
|
72
|
+
{/snippet}
|
|
73
|
+
|
|
74
|
+
<nav class="toc" aria-label={label}>
|
|
75
|
+
<p class="eyebrow">{label}</p>
|
|
76
|
+
<div class="list" bind:this={list}>
|
|
77
|
+
{#if marker}
|
|
78
|
+
<span
|
|
79
|
+
class="marker"
|
|
80
|
+
style:transform="translateY({marker.top}px)"
|
|
81
|
+
style:height="{marker.height}px"
|
|
82
|
+
></span>
|
|
83
|
+
{/if}
|
|
84
|
+
{@render tree(entries)}
|
|
85
|
+
</div>
|
|
86
|
+
</nav>
|
|
87
|
+
|
|
88
|
+
<style>
|
|
89
|
+
.toc {
|
|
90
|
+
font-family: var(--font-ui);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.eyebrow {
|
|
94
|
+
margin: 0 0 0.8rem;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.list {
|
|
98
|
+
position: relative;
|
|
99
|
+
border-left: 1px solid var(--rule);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.marker {
|
|
103
|
+
position: absolute;
|
|
104
|
+
left: -1px;
|
|
105
|
+
top: 0;
|
|
106
|
+
width: 2px;
|
|
107
|
+
background: var(--accent);
|
|
108
|
+
transition:
|
|
109
|
+
transform 180ms ease,
|
|
110
|
+
height 180ms ease;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
ul {
|
|
114
|
+
list-style: none;
|
|
115
|
+
margin: 0;
|
|
116
|
+
padding: 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
ul ul {
|
|
120
|
+
padding-left: 0.8rem;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
a {
|
|
124
|
+
display: block;
|
|
125
|
+
padding: 0.28rem 0 0.28rem 0.9rem;
|
|
126
|
+
font-size: 0.82rem;
|
|
127
|
+
line-height: 1.4;
|
|
128
|
+
color: var(--muted);
|
|
129
|
+
text-decoration: none;
|
|
130
|
+
transition: color 120ms;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.depth-1 > a {
|
|
134
|
+
color: var(--ink-soft);
|
|
135
|
+
font-weight: 500;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
a:hover {
|
|
139
|
+
color: var(--ink);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
a.active {
|
|
143
|
+
color: var(--accent);
|
|
144
|
+
}
|
|
145
|
+
</style>
|
package/dist/fonts.d.ts
ADDED
package/dist/fonts.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The typefaces the default variables name. Import this module once, from a
|
|
2
|
+
// root layout, to load them; a site that sets its own fonts can skip it.
|
|
3
|
+
import '@fontsource-variable/source-serif-4/opsz.css';
|
|
4
|
+
import '@fontsource-variable/source-serif-4/opsz-italic.css';
|
|
5
|
+
import '@fontsource-variable/inter/wght.css';
|
|
6
|
+
import '@fontsource/fira-mono/400.css';
|
package/dist/fuzzy.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface Match {
|
|
2
|
+
score: number;
|
|
3
|
+
indices: number[];
|
|
4
|
+
}
|
|
5
|
+
export declare function fuzzy(query: string, text: string): Match | null;
|
|
6
|
+
export declare function bestMatch(query: string, fields: Array<[text: string, weight: number]>): (Match & {
|
|
7
|
+
field: number;
|
|
8
|
+
}) | null;
|
|
9
|
+
export declare function highlight(text: string, indices: number[]): Array<{
|
|
10
|
+
text: string;
|
|
11
|
+
hit: boolean;
|
|
12
|
+
}>;
|
package/dist/fuzzy.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// A small fuzzy matcher for the index and the search palette: every character
|
|
2
|
+
// of the query must appear in order, and a match scores higher the more of it
|
|
3
|
+
// falls at the start of words and in runs.
|
|
4
|
+
const isBoundary = (text, i) => i == 0 || /[\s\-_/.(:,]/.test(text[i - 1]);
|
|
5
|
+
export function fuzzy(query, text) {
|
|
6
|
+
const q = query.trim().toLowerCase();
|
|
7
|
+
if (!q)
|
|
8
|
+
return { score: 0, indices: [] };
|
|
9
|
+
const t = text.toLowerCase();
|
|
10
|
+
// A plain substring is the strongest kind of match.
|
|
11
|
+
const at = t.indexOf(q);
|
|
12
|
+
if (at >= 0) {
|
|
13
|
+
const indices = Array.from({ length: q.length }, (_, i) => at + i);
|
|
14
|
+
return { score: 100 + q.length * 4 + (isBoundary(t, at) ? 30 : 0) - at * 0.1, indices };
|
|
15
|
+
}
|
|
16
|
+
const indices = [];
|
|
17
|
+
let score = 0;
|
|
18
|
+
let run = 0;
|
|
19
|
+
let from = 0;
|
|
20
|
+
for (const char of q) {
|
|
21
|
+
if (char == ' ')
|
|
22
|
+
continue;
|
|
23
|
+
const i = t.indexOf(char, from);
|
|
24
|
+
if (i < 0)
|
|
25
|
+
return null;
|
|
26
|
+
run = indices.length && i == indices.at(-1) + 1 ? run + 1 : 0;
|
|
27
|
+
score += 1 + run * 2 + (isBoundary(t, i) ? 4 : 0) - Math.min(i - from, 10) * 0.15;
|
|
28
|
+
indices.push(i);
|
|
29
|
+
from = i + 1;
|
|
30
|
+
}
|
|
31
|
+
return { score, indices };
|
|
32
|
+
}
|
|
33
|
+
// The best match over several fields, each weighted.
|
|
34
|
+
export function bestMatch(query, fields) {
|
|
35
|
+
let best = null;
|
|
36
|
+
fields.forEach(([text, weight], field) => {
|
|
37
|
+
const match = fuzzy(query, text);
|
|
38
|
+
if (match && (!best || match.score * weight > best.score)) {
|
|
39
|
+
best = { score: match.score * weight, indices: match.indices, field };
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
return best;
|
|
43
|
+
}
|
|
44
|
+
// `text` cut into runs, marking those that matched.
|
|
45
|
+
export function highlight(text, indices) {
|
|
46
|
+
const hits = new Set(indices);
|
|
47
|
+
const parts = [];
|
|
48
|
+
for (let i = 0; i < text.length; i++) {
|
|
49
|
+
const hit = hits.has(i);
|
|
50
|
+
const last = parts.at(-1);
|
|
51
|
+
if (last && last.hit == hit)
|
|
52
|
+
last.text += text[i];
|
|
53
|
+
else
|
|
54
|
+
parts.push({ text: text[i], hit });
|
|
55
|
+
}
|
|
56
|
+
return parts;
|
|
57
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type Simulation, type SimulationLinkDatum, type SimulationNodeDatum } from 'd3-force';
|
|
2
|
+
export interface GraphTuning {
|
|
3
|
+
charge: number;
|
|
4
|
+
linkDistance: number;
|
|
5
|
+
gravity: number;
|
|
6
|
+
}
|
|
7
|
+
export declare const DEFAULT_TUNING: GraphTuning;
|
|
8
|
+
export interface LayoutNode extends SimulationNodeDatum {
|
|
9
|
+
id: string;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
depth: number;
|
|
13
|
+
}
|
|
14
|
+
export interface LayoutEdge extends SimulationLinkDatum<LayoutNode> {
|
|
15
|
+
source: string | LayoutNode;
|
|
16
|
+
target: string | LayoutNode;
|
|
17
|
+
style: 'solid' | 'dashed';
|
|
18
|
+
}
|
|
19
|
+
export declare const BAND = 92;
|
|
20
|
+
export declare const NODE_HEIGHT = 30;
|
|
21
|
+
export declare const estimateWidth: (label: string) => number;
|
|
22
|
+
export declare function depths(ids: string[], edges: Array<{
|
|
23
|
+
from: string;
|
|
24
|
+
to: string;
|
|
25
|
+
style: string;
|
|
26
|
+
}>): Map<string, number>;
|
|
27
|
+
export declare function createSimulation(nodes: LayoutNode[], edges: LayoutEdge[], tuning: GraphTuning): Simulation<LayoutNode, LayoutEdge>;
|
|
28
|
+
export declare function layout(nodes: LayoutNode[], edges: LayoutEdge[], tuning: GraphTuning, ticks?: number): Map<string, {
|
|
29
|
+
x: number;
|
|
30
|
+
y: number;
|
|
31
|
+
}>;
|
|
32
|
+
export declare function boxExit(cx: number, cy: number, width: number, height: number, x: number, y: number, margin?: number): {
|
|
33
|
+
x: number;
|
|
34
|
+
y: number;
|
|
35
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { forceLink, forceManyBody, forceSimulation, forceX, forceY, } from 'd3-force';
|
|
2
|
+
export const DEFAULT_TUNING = { charge: -900, linkDistance: 80, gravity: 0.02 };
|
|
3
|
+
// Vertical distance between dependency bands.
|
|
4
|
+
export const BAND = 92;
|
|
5
|
+
export const NODE_HEIGHT = 30;
|
|
6
|
+
// A label's width before the browser can measure it: an average glyph width
|
|
7
|
+
// for the interface font at the graph's size, plus padding.
|
|
8
|
+
export const estimateWidth = (label) => Math.round(Math.min(label.length, 42) * 6.9 + 28);
|
|
9
|
+
// How deep each node sits: the longest chain of solid edges leading into it.
|
|
10
|
+
// A cycle --- which the content should not have, but might --- is cut where
|
|
11
|
+
// the walk first comes back on itself.
|
|
12
|
+
export function depths(ids, edges) {
|
|
13
|
+
const into = new Map(ids.map((id) => [id, []]));
|
|
14
|
+
for (const edge of edges)
|
|
15
|
+
if (edge.style == 'solid')
|
|
16
|
+
into.get(edge.to)?.push(edge.from);
|
|
17
|
+
const depth = new Map();
|
|
18
|
+
const visiting = new Set();
|
|
19
|
+
const visit = (id) => {
|
|
20
|
+
const known = depth.get(id);
|
|
21
|
+
if (known !== undefined)
|
|
22
|
+
return known;
|
|
23
|
+
if (visiting.has(id))
|
|
24
|
+
return 0;
|
|
25
|
+
visiting.add(id);
|
|
26
|
+
const parents = into.get(id) ?? [];
|
|
27
|
+
const value = parents.length ? 1 + Math.max(...parents.map(visit)) : 0;
|
|
28
|
+
visiting.delete(id);
|
|
29
|
+
depth.set(id, value);
|
|
30
|
+
return value;
|
|
31
|
+
};
|
|
32
|
+
ids.forEach(visit);
|
|
33
|
+
return depth;
|
|
34
|
+
}
|
|
35
|
+
// Keeps labels from overlapping: any two boxes that would touch are pushed
|
|
36
|
+
// apart along whichever axis they overlap least.
|
|
37
|
+
function forceBoxes(padding = 10) {
|
|
38
|
+
let nodes = [];
|
|
39
|
+
const force = (alpha) => {
|
|
40
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
41
|
+
const a = nodes[i];
|
|
42
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
43
|
+
const b = nodes[j];
|
|
44
|
+
const dx = (b.x ?? 0) - (a.x ?? 0);
|
|
45
|
+
const dy = (b.y ?? 0) - (a.y ?? 0);
|
|
46
|
+
const overlapX = (a.width + b.width) / 2 + padding - Math.abs(dx);
|
|
47
|
+
const overlapY = (a.height + b.height) / 2 + padding - Math.abs(dy);
|
|
48
|
+
if (overlapX <= 0 || overlapY <= 0)
|
|
49
|
+
continue;
|
|
50
|
+
const strength = Math.min(1, alpha * 4);
|
|
51
|
+
if (overlapX < overlapY) {
|
|
52
|
+
const shift = (overlapX / 2) * strength * (dx < 0 ? -1 : 1);
|
|
53
|
+
a.x = (a.x ?? 0) - shift;
|
|
54
|
+
b.x = (b.x ?? 0) + shift;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const shift = (overlapY / 2) * strength * (dy < 0 ? -1 : 1);
|
|
58
|
+
a.y = (a.y ?? 0) - shift;
|
|
59
|
+
b.y = (b.y ?? 0) + shift;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
force.initialize = (given) => (nodes = given);
|
|
65
|
+
return force;
|
|
66
|
+
}
|
|
67
|
+
export function createSimulation(nodes, edges, tuning) {
|
|
68
|
+
const deepest = Math.max(0, ...nodes.map((node) => node.depth));
|
|
69
|
+
const bandOf = (node) => (node.depth - deepest / 2) * BAND;
|
|
70
|
+
return forceSimulation(nodes)
|
|
71
|
+
.force('links', forceLink(edges)
|
|
72
|
+
.id((node) => node.id)
|
|
73
|
+
.distance((edge) => edge.style == 'solid' ? tuning.linkDistance : tuning.linkDistance * 2.2)
|
|
74
|
+
.strength((edge) => (edge.style == 'solid' ? 0.6 : 0.06)))
|
|
75
|
+
.force('charge', forceManyBody().strength(tuning.charge).distanceMax(700))
|
|
76
|
+
.force('bands', forceY(bandOf).strength(0.5))
|
|
77
|
+
.force('gravity', forceX(0).strength(tuning.gravity))
|
|
78
|
+
.force('gravityY', forceY(0).strength(tuning.gravity / 4))
|
|
79
|
+
.force('boxes', forceBoxes());
|
|
80
|
+
}
|
|
81
|
+
// Runs the simulation to rest and returns where everything ended up.
|
|
82
|
+
export function layout(nodes, edges, tuning, ticks = 400) {
|
|
83
|
+
const simulation = createSimulation(nodes, edges, tuning).stop();
|
|
84
|
+
simulation.tick(ticks);
|
|
85
|
+
return new Map(nodes.map((node) => [node.id, { x: node.x ?? 0, y: node.y ?? 0 }]));
|
|
86
|
+
}
|
|
87
|
+
// The point where the segment from a box's centre towards (x, y) leaves the
|
|
88
|
+
// box, so that an edge starts and ends at a label's border.
|
|
89
|
+
export function boxExit(cx, cy, width, height, x, y, margin = 3) {
|
|
90
|
+
const dx = x - cx;
|
|
91
|
+
const dy = y - cy;
|
|
92
|
+
if (dx == 0 && dy == 0)
|
|
93
|
+
return { x: cx, y: cy };
|
|
94
|
+
const sx = (width / 2 + margin) / Math.abs(dx || 1e-9);
|
|
95
|
+
const sy = (height / 2 + margin) / Math.abs(dy || 1e-9);
|
|
96
|
+
const s = Math.min(sx, sy, 1);
|
|
97
|
+
return { x: cx + dx * s, y: cy + dy * s };
|
|
98
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export { default as Article } from './components/Article.svelte';
|
|
2
|
+
export { default as ArticleHeader } from './components/ArticleHeader.svelte';
|
|
3
|
+
export { default as Chips } from './components/Chips.svelte';
|
|
4
|
+
export { default as Graph, MESEARCH_LEGEND } from './components/Graph.svelte';
|
|
5
|
+
export { default as GraphPanel } from './components/GraphPanel.svelte';
|
|
6
|
+
export { default as Icon } from './components/Icon.svelte';
|
|
7
|
+
export { default as Index } from './components/Index.svelte';
|
|
8
|
+
export { default as Pager } from './components/Pager.svelte';
|
|
9
|
+
export { default as Proof } from './components/Proof.svelte';
|
|
10
|
+
export { default as References } from './components/References.svelte';
|
|
11
|
+
export { default as Palette } from './components/Palette.svelte';
|
|
12
|
+
export { default as Parts } from './components/Parts.svelte';
|
|
13
|
+
export { default as Rail } from './components/Rail.svelte';
|
|
14
|
+
export { default as SequenceTrack } from './components/SequenceTrack.svelte';
|
|
15
|
+
export { default as Shell } from './components/Shell.svelte';
|
|
16
|
+
export { default as Statement } from './components/Statement.svelte';
|
|
17
|
+
export { default as ThemeToggle } from './components/ThemeToggle.svelte';
|
|
18
|
+
export { default as Toc } from './components/Toc.svelte';
|
|
19
|
+
export { configure, formatDate, isoDate, kindLabel, plain, settings } from './settings.js';
|
|
20
|
+
export { theme, THEME_SCRIPT, type ThemeChoice } from './theme.svelte.js';
|
|
21
|
+
export { reading } from './reading.svelte.js';
|
|
22
|
+
export { bestMatch, fuzzy, highlight } from './fuzzy.js';
|
|
23
|
+
export { DEFAULT_TUNING, type GraphTuning } from './graph-layout.js';
|
|
24
|
+
export type * from './types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// The pieces mesearch sites are made of, for any Svelte site to use. Data and
|
|
2
|
+
// URLs are always passed in, so nothing here knows how a site stores its
|
|
3
|
+
// content or lays out its pages.
|
|
4
|
+
//
|
|
5
|
+
// Styles: import '@mvarble/mesearch-ui/styles/index.css' once, from a root
|
|
6
|
+
// layout, along with KaTeX's stylesheet and, for the default typefaces,
|
|
7
|
+
// '@mvarble/mesearch-ui/fonts'.
|
|
8
|
+
export { default as Article } from './components/Article.svelte';
|
|
9
|
+
export { default as ArticleHeader } from './components/ArticleHeader.svelte';
|
|
10
|
+
export { default as Chips } from './components/Chips.svelte';
|
|
11
|
+
export { default as Graph, MESEARCH_LEGEND } from './components/Graph.svelte';
|
|
12
|
+
export { default as GraphPanel } from './components/GraphPanel.svelte';
|
|
13
|
+
export { default as Icon } from './components/Icon.svelte';
|
|
14
|
+
export { default as Index } from './components/Index.svelte';
|
|
15
|
+
export { default as Pager } from './components/Pager.svelte';
|
|
16
|
+
export { default as Proof } from './components/Proof.svelte';
|
|
17
|
+
export { default as References } from './components/References.svelte';
|
|
18
|
+
export { default as Palette } from './components/Palette.svelte';
|
|
19
|
+
export { default as Parts } from './components/Parts.svelte';
|
|
20
|
+
export { default as Rail } from './components/Rail.svelte';
|
|
21
|
+
export { default as SequenceTrack } from './components/SequenceTrack.svelte';
|
|
22
|
+
export { default as Shell } from './components/Shell.svelte';
|
|
23
|
+
export { default as Statement } from './components/Statement.svelte';
|
|
24
|
+
export { default as ThemeToggle } from './components/ThemeToggle.svelte';
|
|
25
|
+
export { default as Toc } from './components/Toc.svelte';
|
|
26
|
+
export { configure, formatDate, isoDate, kindLabel, plain, settings } from './settings.js';
|
|
27
|
+
export { theme, THEME_SCRIPT } from './theme.svelte.js';
|
|
28
|
+
export { reading } from './reading.svelte.js';
|
|
29
|
+
export { bestMatch, fuzzy, highlight } from './fuzzy.js';
|
|
30
|
+
export { DEFAULT_TUNING } from './graph-layout.js';
|