@ape-egg/codecane 0.0.0-stage → 1.0.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.
@@ -0,0 +1,25 @@
1
+ <header @[fold] stylecheat dark away="@[fullscreen && !returning]">
2
+ <a href="/" aria-current="@[root ? 'page' : 'false']"><strong>@[name]</strong></a>
3
+ <ol style="grid-template-rows: @[rows]">
4
+ <!-- each cells as cell (cell.key) -->
5
+ <li style="grid-area: @[cell.lane + 1] / @[cell.depth + 1] / span @[cell.span]" @[cell.shown]>
6
+ <!-- if cell.follows -->
7
+ <arrow>→</arrow>
8
+ <!-- /if -->
9
+ <a href="/@[cell.address]" onclick="codecane.pick(event, '@[cell.id]', @[cell.lane], @[cell.span])" aria-current="@[page.id === cell.id ? 'step' : 'false']" kind="@[steps[cell.id].kind ?? '']" @[cell.lit]>
10
+ <!-- if steps[cell.id].kind -->
11
+ <join aria-hidden="true">Y</join>
12
+ <!-- /if -->
13
+ <!-- if steps[cell.id].kind !== 'seam' -->
14
+ @[steps[cell.id].title]
15
+ <!-- /if -->
16
+ </a>
17
+ <!-- if cell.forks -->
18
+ <button fork title="@[cell.forks] timelines fork after @[steps[cell.id].title]" aria-expanded="@[fold && through === cell.id ? 'true' : 'false']" onclick="codecane.fold('@[cell.id]')"><split aria-hidden="true">Y</split>@[cell.forks]</button>
19
+ <!-- /if -->
20
+ </li>
21
+ <!-- /each -->
22
+ </ol>
23
+ <logotext-codecane><a href="https://codecane.korte.kim" target="_blank">code<span>cane</span></a><version>v@[version]</version></logotext-codecane>
24
+ <button onclick="codecane.expand()" aria-label="Fullscreen" tertiary sm><icon fullscreen></icon></button>
25
+ </header>
package/ui/index.html ADDED
@@ -0,0 +1,20 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>codecane</title>
7
+ <link rel="stylesheet" href="/node_modules/@ape-egg/codie/codie.css">
8
+ <link rel="stylesheet" href="/codecane/ui/stylecheat.css">
9
+ <link rel="stylesheet" href="/codecane/ui/codecane.css">
10
+ <script type="module" src="/codecane/ui/viewer.js"></script>
11
+ </head>
12
+ <body vibe-fouc>
13
+ <component src="/codecane/ui/components/Timeline.html"></component>
14
+ <workspace fullscreen="@[fullscreen]" away="@[fullscreen && !returning]">
15
+ <component src="/codecane/ui/components/Story.html"></component>
16
+ <component src="/codecane/ui/components/Stage.html"></component>
17
+ <component src="/codecane/ui/components/Changes.html"></component>
18
+ </workspace>
19
+ </body>
20
+ </html>
package/ui/mounted.js ADDED
@@ -0,0 +1,73 @@
1
+ const blobs = {}
2
+ const sources = {}
3
+ const blob = (text, type, path) => {
4
+ const url = blobs[`${path}\n${text}`] ??= URL.createObjectURL(new Blob([text], { type }))
5
+ sources[url] = path
6
+ return url
7
+ }
8
+
9
+ const moduleLinks = /((?:from|import)\s*\(?\s*)(['"])(\.{1,2}\/[^'"]+)\2/g
10
+ const styleLinks = /((?:url\(|@import\s+(?!url\())\s*)(['"]?)([^'")\s;]+)\2/g
11
+
12
+ const reporter = `{
13
+ let failed = false
14
+ const fail = failure => {
15
+ failed = true
16
+ parent.postMessage({ codecane: 'failed', ...failure }, origin)
17
+ }
18
+ addEventListener('error', event => fail(event instanceof ErrorEvent
19
+ ? { message: String(event.error ?? event.message), source: event.filename, line: event.lineno }
20
+ : { message: 'Could not load ' + (event.target.src ?? event.target.href), source: '' }), true)
21
+ addEventListener('unhandledrejection', ({ reason }) => fail({ message: String(reason), source: '' }))
22
+ addEventListener('load', () => failed || parent.postMessage({ codecane: 'ran' }, origin))
23
+ }`
24
+
25
+ export const pathOf = (page, url) => sources[url] ?? (url.startsWith(page.base) ? url.split(/[?#]/)[0].slice(page.base.length) : url)
26
+
27
+ const sheets = (page, drafts, text) => {
28
+ const address = path => new URL(path, page.base).href
29
+ const has = path => page.files.some(file => file.path === path)
30
+ const styles = {}
31
+ const styled = path => styles[path] ??= (async () => {
32
+ const css = drafts[path] ?? await text(path)
33
+ const targets = Object.fromEntries(await Promise.all([...css.matchAll(styleLinks)].map(async ([, , , link]) => {
34
+ const target = new URL(link, address(path)).href
35
+ return [link, pathOf(page, target).endsWith('.css') && has(pathOf(page, target)) ? await styled(pathOf(page, target)) : target]
36
+ })))
37
+ return blob(css.replace(styleLinks, (_, lead, quote, link) => `${lead}${quote}${targets[link]}${quote}`), 'text/css', path)
38
+ })()
39
+ return { address, has, styled }
40
+ }
41
+
42
+ export const restyled = (page, drafts, text) => {
43
+ const { has, styled } = sheets(page, drafts, text)
44
+ return async href => has(pathOf(page, href)) ? styled(pathOf(page, href)) : href
45
+ }
46
+
47
+ export const mounted = async (page, drafts, text) => {
48
+ const { address, has, styled } = sheets(page, drafts, text)
49
+
50
+ const modules = Object.fromEntries(Object.keys(drafts).filter(path => path.endsWith('.js')).map(path => [
51
+ address(path),
52
+ blob(drafts[path].replace(moduleLinks, (_, lead, quote, link) => `${lead}${quote}${new URL(link, address(path))}${quote}`), 'text/javascript', path),
53
+ ]))
54
+
55
+ const doc = new DOMParser().parseFromString(drafts['index.html'] ?? await text('index.html'), 'text/html')
56
+ const report = Object.assign(doc.createElement('script'), { textContent: reporter })
57
+ const base = Object.assign(doc.createElement('base'), { href: page.base })
58
+ const map = doc.querySelector('script[type="importmap"]') ?? Object.assign(doc.createElement('script'), { type: 'importmap', textContent: '{}' })
59
+ const { imports, ...rest } = JSON.parse(map.textContent)
60
+ map.textContent = JSON.stringify({ ...rest, imports: { ...imports, ...modules } })
61
+ doc.head.prepend(report, base, map)
62
+
63
+ await Promise.all([...doc.querySelectorAll('link[rel="stylesheet"][href]')].map(async link => {
64
+ const path = pathOf(page, new URL(link.getAttribute('href'), page.base).href)
65
+ if (has(path)) link.setAttribute('href', await styled(path))
66
+ }))
67
+ doc.querySelectorAll('script[type="module"][src]').forEach(script => {
68
+ const target = new URL(script.getAttribute('src'), page.base).href
69
+ script.setAttribute('src', modules[target] ?? target)
70
+ })
71
+
72
+ return blob(`<!doctype html>\n${doc.documentElement.outerHTML}`, 'text/html', 'index.html')
73
+ }