diff options
Diffstat (limited to 'tools/copland')
37 files changed, 2648 insertions, 0 deletions
diff --git a/tools/copland/dist/copland-skin.config b/tools/copland/dist/copland-skin.config new file mode 100644 index 0000000..02392e8 --- /dev/null +++ b/tools/copland/dist/copland-skin.config @@ -0,0 +1,827 @@ +# Copland Fossil skin configuration +# Generated deterministically by tools/copland/scripts/build-config.mjs. +# Import with: fossil configuration import dist/copland-skin.config -R REPOSITORY +config /config 12869 +1 'css' value ':root { + color-scheme: light dark; + --page-bg: #fff; + --surface: #fff; + --surface-subtle: #f5f5f5; + --surface-muted: #e9ecef; + --surface-active: #ddd; + --text: #212529; + --text-muted: #6c757d; + --text-faint: #767676; + --border: #ced4da; + --border-strong: #888; + --link: #0645d9; + --link-hover: #002f9f; + --primary: #0056d6; + --primary-hover: #0044aa; + --primary-text: #fff; + --danger: #b00020; + --danger-bg: #f8d7da; + --success: #146c2e; + --success-bg: #d4edda; + --warning-bg: #fff3cd; + --info-bg: #d1ecf1; + --code-bg: #f1f3f5; + --selection: #b9d6ff; + --focus: #ffbf47; + --shadow: rgba(0, 0, 0, 0.12); + --container: 71.25rem; + --gutter: 1rem; + --radius: 0; + --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; +} + +@media (prefers-color-scheme: dark) { + :root { + --page-bg: #212529; + --surface: #212529; + --surface-subtle: #191c1f; + --surface-muted: #2d3237; + --surface-active: #3b4147; + --text: #f1f3f5; + --text-muted: #c2c7cc; + --text-faint: #adb5bd; + --border: #565e65; + --border-strong: #8b949c; + --link: #5c9cff; + --link-hover: #91bcff; + --primary: #4d8ee8; + --primary-hover: #70a7f2; + --primary-text: #101214; + --danger: #ff6b78; + --danger-bg: #57242a; + --success: #65d47d; + --success-bg: #183d24; + --warning-bg: #514318; + --info-bg: #193e47; + --code-bg: #17191b; + --selection: #173f70; + --focus: #ffd166; + --shadow: rgba(0, 0, 0, 0.4); + } +} +* { + box-sizing: border-box; +} + +html { + min-height: 100%; + scrollbar-gutter: stable; + font-size: 16px; +} + +body { + min-height: 100vh; + margin: 0; + padding: 0 0 1rem; + background: var(--page-bg); + color: var(--text); + font-family: var(--font-sans); + font-size: 1rem; + line-height: 1.5; + text-align: initial; +} + +::selection { + background: var(--selection); +} + +.container, +div.content { + width: 100%; + max-width: var(--container); + margin-inline: auto; + padding-inline: var(--gutter); +} + +div.content { + min-height: 45vh; +} + +body a { + color: var(--link); + text-decoration: underline; + text-underline-offset: 0.12em; +} + +body a:hover { + color: var(--link-hover); + text-decoration: none; +} + +a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible, +textarea:focus-visible, [tabindex]:focus-visible { + outline: 3px solid var(--focus); + outline-offset: 2px; +} + +h1, h2, h3, h4, h5, h6 { + margin: 1rem 0 0.5rem; + color: var(--text); + font-weight: 500; + line-height: 1.2; +} + +h1 { + font-size: 1.75rem; +} + +h2 { + font-size: 1.45rem; +} + +h3 { + font-size: 1.15rem; + border-bottom: 1px solid var(--border); +} + +h4 { + font-size: 1rem; + border-bottom: 1px solid var(--border); +} + +p { + margin: 0 0 1rem; +} + +hr { + border: 0; + border-top: 1px solid var(--border); +} + +.site-header { + margin-bottom: 0; +} + +.site-header-row { + min-height: 3.25rem; + display: flex; + align-items: center; + gap: 1rem; +} + +.site-brand, +.resource-title a, +.account-nav a, +.mainmenu a { + text-decoration: none; +} + +.site-brand { + flex: 0 0 auto; + color: var(--text); + font-size: 1.25rem; + font-weight: 600; + letter-spacing: -0.025em; +} + +.site-brand::before { + content: ""; + display: inline-block; + width: 0.78rem; + height: 0.78rem; + margin-right: 0.42rem; + border: 2px solid currentColor; + border-radius: 50%; + vertical-align: 0.02rem; +} + +.site-brand:hover, +.resource-title a:hover, +.account-nav a:hover, +.mainmenu a:hover { + color: var(--text); + text-decoration: underline; +} + +.resource-title { + min-width: 0; + display: flex; + align-items: baseline; + gap: 0.4rem; + overflow: hidden; + white-space: nowrap; +} + +.resource-title a, +.resource-page { + overflow: hidden; + text-overflow: ellipsis; +} + +.resource-title a { + color: var(--text); + font-weight: 600; +} + +.resource-separator, .resource-page { + color: var(--text-muted); +} + +.account-nav { + margin-left: auto; + white-space: nowrap; + font-size: 0.9rem; +} + +.resource-nav { + border-bottom: 3px solid var(--surface-active); +} + +.mainmenu { + min-height: 2rem; + display: flex; + align-items: end; + gap: 0; + overflow-x: auto; + scrollbar-width: thin; +} + +.mainmenu a { + flex: 0 0 auto; + padding: 0.25rem 0.85rem 0.18rem; + color: var(--text-muted); + white-space: nowrap; +} + +.mainmenu a.active, +.mainmenu a.selected, +.mainmenu a:hover { + color: var(--text); +} + +.mainmenu a.active, +.mainmenu a.selected { + background: var(--surface-active); +} + +.page-title { + margin-top: 0.9rem; + margin-bottom: 0.75rem; + font-size: 1.35rem; + font-weight: 500; +} + +.site-footer { + margin-top: 2rem; + color: var(--text-muted); + font-size: 0.8rem; +} + +.site-footer .container { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.4rem; +} + +pre, code, kbd, samp, tt { + font-family: var(--font-mono); +} + +code, tt { + overflow-wrap: anywhere; +} + +pre { + max-width: 100%; + overflow: auto; + margin: 0 0 1rem; + padding: 0.75rem 1rem; + background: var(--code-bg); + color: var(--text); + font-size: 0.875rem; + line-height: 1.35; + tab-size: 4; +} + +pre a { + color: inherit; +} + +blockquote { + margin: 0 0 1rem; + padding-left: 1rem; + border-left: 4px solid var(--border); +} + +.org-document { + max-width: 100%; +} + +.org-document .org-title { + margin-top: 0; +} + +.org-metadata { + display: grid; + grid-template-columns: max-content minmax(0, 1fr); + gap: 0.15rem 0.75rem; + margin: 0 0 1rem; + color: var(--text-muted); + font-size: 0.92rem; +} + +.org-metadata dt { + font-weight: 600; +} + +.org-metadata dd { + margin: 0; +} + +.org-toc { + margin: 0 0 1.25rem; + padding: 0.75rem 1rem; + border: 1px solid var(--border); + background: var(--surface-subtle); +} + +.org-toc h2 { + margin-top: 0; + font-size: 1.05rem; +} + +.org-toc ol { + margin: 0; + padding-left: 1.5rem; +} + +.org-toc-level-2 { + margin-left: 1rem; +} + +.org-toc-level-3 { + margin-left: 2rem; +} + +.org-toc-level-4, .org-toc-level-5, .org-toc-level-6 { + margin-left: 3rem; +} + +.org-table { + display: block; + overflow-x: auto; +} + +.org-block { + border-left: 3px solid var(--border-strong); +} + +.org-example { + color: var(--text); +} + +.org-unknown-block { + border-left-color: var(--danger); +} + +table { + width: 100%; + margin-bottom: 1rem; + border-collapse: collapse; + color: var(--text); +} + +th { + text-align: left; + font-weight: 600; +} + +th, td { + padding: 0.28rem 0.5rem; + vertical-align: top; + border-top: 1px solid var(--border); +} + +thead th { + border-bottom: 2px solid var(--border-strong); +} + +tbody tr:nth-child(odd) { + background: color-mix(in srgb, var(--surface-subtle) 70%, transparent); +} + +table.browser, table.report, table.timelineTable { + font-size: 0.92rem; +} + +table.browser td:first-child, table.browser td:nth-child(2) { + font-family: var(--font-mono); +} + +input, button, select, textarea { + font: inherit; +} + +input[type=text], input[type=search], input[type=email], input[type=password], +input[type=url], input[type=number], textarea, select { + max-width: 100%; + padding: 0.35rem 0.5rem; + border: 1px solid var(--border-strong); + border-radius: var(--radius); + background: var(--surface); + color: var(--text); +} + +textarea { + width: 100%; +} + +label { + display: inline-block; + margin-bottom: 0.25rem; +} + +button, input[type=submit], input[type=button], .button, a.button { + display: inline-block; + padding: 0.35rem 0.7rem; + border: 1px solid var(--primary); + border-radius: var(--radius); + background: var(--primary); + color: var(--primary-text); + cursor: pointer; + line-height: 1.25; + text-decoration: none; +} + +button:hover, input[type=submit]:hover, input[type=button]:hover, .button:hover { + border-color: var(--primary-hover); + background: var(--primary-hover); +} + +button:disabled, input:disabled { + cursor: not-allowed; + opacity: 0.65; +} + +fieldset { + margin: 0 0 1rem; + border: 1px solid var(--border); +} + +legend { + padding-inline: 0.35rem; +} + +.submenu, .sectionmenu { + margin: 0 0 1rem; + padding: 0.4rem 0.6rem; + background: var(--surface-subtle); + border: 1px solid var(--border); +} + +.submenu a, .sectionmenu a { + margin-right: 0.75rem; +} + +.generalError, .error, .fatalError, .loginError, +.warning, .notice, .success { + margin: 0.75rem 0; + padding: 0.65rem 0.8rem; + border: 1px solid var(--border); +} + +.generalError, .error, .fatalError, .loginError { + background: var(--danger-bg); + color: var(--text); +} + +.warning { + background: var(--warning-bg); +} + +.notice { + background: var(--info-bg); +} + +.success { + background: var(--success-bg); +} + +.timelineDate { + margin-top: 1rem; + color: var(--text); + font-weight: 600; +} + +.timelineHistDsp { + color: var(--text-muted); +} + +.timelineModernCell, .timelineColumnarCell, .timelineDetailCell { + padding-block: 0.35rem; +} + +.timelineModernDetail { + opacity: 1; +} + +.timelineCompactComment, .timelineComment { + line-height: 1.35; +} + +.timelineTime { + color: var(--text-muted); + white-space: nowrap; +} + +.timelineGraph { + min-width: 2rem; +} + +.tl-node { + border-radius: 50%; +} + +.timelineModernCell a, +.timelineColumnarCell a, +.timelineDetailCell a { + color: var(--link) !important; +} + +.timelineModernCell a:hover, +.timelineColumnarCell a:hover, +.timelineDetailCell a:hover { + color: var(--link-hover) !important; +} + +.filetree { + font-family: var(--font-mono); + font-size: 0.9rem; +} + +.filetree ul { + padding-left: 1.25rem; +} + +.filetree a { + color: var(--text); +} + +.filetree a:hover { + color: var(--link); +} + +.fileage tr:hover, table.browser tr:hover { + background: var(--surface-active); +} + +.diff, .udiff, .sbsdiff { + font-family: var(--font-mono); + font-size: 0.82rem; +} + +.diff pre, .udiff pre, .sbsdiff pre { + background: transparent; +} + +.diffln, .diffhr { + color: var(--text-muted); + background: var(--surface-muted); +} + +.diffadd, .diffins { + background: var(--success-bg); +} + +.diffrm, .diffdel { + background: var(--danger-bg); +} + +.diffchng { + background: var(--warning-bg); +} + +.selectedText { + background: var(--selection); +} + +.forumPost, .forumTime, .forumSel, .forumHierRoot, .forumHier { + border-color: var(--border); +} + +.forumPost, .forumTime { + background: var(--surface-subtle); + color: var(--text); +} + +.forumPostBody { + overflow-wrap: anywhere; +} + +.forumPostBody img, .wiki-content img, .markdown img { + max-width: 100%; + height: auto; +} + +.ticketDspLabel, .ticketDspValue { + padding: 0.35rem 0.5rem; +} + +.ticketDspLabel { + color: var(--text-muted); + font-weight: 600; +} + +.ticketDspValue { + border-bottom: 1px solid var(--border); +} + +.branchname, .tagname, .wikiTagCancelled, .infoTagCancelled { + display: inline-block; + padding: 0.05rem 0.3rem; + background: var(--surface-muted); + color: var(--text); + font-size: 0.82rem; +} + +.statistics-report-table-events th, .statistics-report-table-events td { + text-align: right; +} + +.statistics-report-table-events th:first-child, .statistics-report-table-events td:first-child { + text-align: left; +} + +.accordion_panel { + border-color: var(--border); + background: var(--surface); +} + +.accordion_marker { + color: var(--text-muted); +} + +body.repolist .resource-title, +body.repolist .resource-nav, +body.repolist > .page-title { + display: none; +} + +body.repolist div.content { + padding-top: 1rem; +} + +body.repolist table td:first-child { + font-family: var(--font-mono); + font-weight: 600; +} + +@media (prefers-color-scheme: dark) { + img { + opacity: 0.92; + } + body .timelineModernCell, + body .timelineColumnarCell, + body .timelineDetailCell, + .timelineModernCell[style*=background-color], + .timelineColumnarCell[style*=background-color], + .timelineDetailCell[style*=background-color], + td[style*=background-color] { + background-color: var(--surface-subtle) !important; + color: var(--text) !important; + } + .diffadd, .diffins { + color: var(--text); + } + .diffrm, .diffdel { + color: var(--text); + } +} +@media (max-width: 48rem) { + :root { + --gutter: 0.75rem; + } + .site-header-row { + min-height: 2.8rem; + gap: 0.65rem; + } + .resource-page, .resource-separator { + display: none; + } + .mainmenu a { + padding-inline: 0.65rem; + } + .page-title { + font-size: 1.2rem; + } + table:not(.sbsdiffcols) { + display: block; + max-width: 100%; + overflow-x: auto; + } + .desktoponly { + display: none !important; + } +} +@media (max-width: 36rem) { + .site-brand { + font-size: 1.1rem; + } + .resource-title { + font-size: 0.9rem; + } + .account-nav { + font-size: 0.82rem; + } + .page-title { + margin-top: 0.7rem; + } + pre { + padding: 0.6rem 0.7rem; + } +} +@media print { + .resource-nav, .account-nav, .submenu, .sectionmenu, .site-footer { + display: none !important; + } + body, pre, code { + background: #fff !important; + color: #000 !important; + } + a { + color: #000; + text-decoration: underline; + } + div.content, .container { + max-width: none; + padding: 0; + } +} +' +config /config 1631 +1 'header' value '<header class="site-header"> + <div class="container site-header-row"> + <a class="site-brand" href="$home$index_page">Copland</a> + <div class="resource-title"> + <a href="$home$index_page">$<project_name></a> + <span class="resource-separator" aria-hidden="true">/</span> + <span class="resource-page">$<title></span> + </div> + <div class="account-nav"> + <th1> + if {[info exists login]} { + html "<a href=''$home/login''>$login</a>" + } else { + html "<a href=''$home/login''>Log in</a>" + } + </th1> + </div> + </div> + <div class="resource-nav"> + <nav class="container mainmenu" aria-label="Repository"> + <th1> + foreach {name url expr class} $mainmenu { + if {![capexpr $expr]} continue + if {[string match /chat* $url] || [string match chat* $url] + || [string match /taglist* $url] || [string match taglist* $url] + || [string match /forum* $url] || [string match forum* $url] + || [string match /reportlist* $url] || [string match reportlist* $url] + || [string match /ticket* $url] || [string match ticket* $url] + || [string match /tkt* $url] || [string match tkt* $url]} continue + if {[string match /* $url]} { + if {[string match $url\[/?#\]* /$current_page/]} { + set class "active $class" + } + set url $home$url + } + html "<a href=''$url'' class=''$class''>$name</a>\n" + } + </th1> + </nav> + </div> +</header> +<h1 class="container page-title">$<title></h1> +' +config /config 351 +1 'footer' value '<footer class="site-footer"> + <div class="container"> + <span>Copland</span> + <span aria-hidden="true">·</span> + <span>Powered by Fossil $release_version</span> + <span aria-hidden="true">·</span> + <span>Generated in <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s</span> + </div> +</footer> + +' +config /config 271 +1 'details' value 'pikchr-background: "" +pikchr-fontscale: "0.9" +pikchr-foreground: "" +pikchr-scale: "1.1" +timeline-arrowheads: 1 +timeline-circle-nodes: 1 +timeline-color-graph-lines: 1 +white-foreground: 0 + +' +config /config 16 +1 'js' value ' +' diff --git a/tools/copland/package-lock.json b/tools/copland/package-lock.json new file mode 100644 index 0000000..6388ad6 --- /dev/null +++ b/tools/copland/package-lock.json @@ -0,0 +1,549 @@ +{ + "name": "copland-fossil-skin", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "copland-fossil-skin", + "version": "0.1.0", + "devDependencies": { + "@axe-core/playwright": "4.13.0", + "@playwright/test": "1.61.1", + "sass": "1.103.0" + } + }, + "node_modules/@axe-core/playwright": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.13.0.tgz", + "integrity": "sha512-6YLx+kxXu5GJceG4ozFg+33a2EMTdjYwWGloJ3sb9Kta5pp+ZNS53uxGVog5JetIY8s++P5UrtX+cri+u0VAVg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "axe-core": "~4.13.0" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", + "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.6.0", + "@parcel/watcher-darwin-arm64": "2.6.0", + "@parcel/watcher-darwin-x64": "2.6.0", + "@parcel/watcher-freebsd-x64": "2.6.0", + "@parcel/watcher-linux-arm-glibc": "2.6.0", + "@parcel/watcher-linux-arm-musl": "2.6.0", + "@parcel/watcher-linux-arm64-glibc": "2.6.0", + "@parcel/watcher-linux-arm64-musl": "2.6.0", + "@parcel/watcher-linux-x64-glibc": "2.6.0", + "@parcel/watcher-linux-x64-musl": "2.6.0", + "@parcel/watcher-win32-arm64": "2.6.0", + "@parcel/watcher-win32-x64": "2.6.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", + "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", + "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", + "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", + "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", + "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", + "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", + "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", + "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", + "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", + "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", + "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", + "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/axe-core": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.13.0.tgz", + "integrity": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/immutable": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright/node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass": { + "version": "1.103.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.103.0.tgz", + "integrity": "sha512-+QpdXUDw19lVqRDlYIyje1Lq/0gHsnNHIl4x1CqeUg13zRhaQN11UvTvouwHWLXc9Q3rQVT6oBhFGRYJ5Z8gHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=20.19.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + } + } +} diff --git a/tools/copland/package.json b/tools/copland/package.json new file mode 100644 index 0000000..da314aa --- /dev/null +++ b/tools/copland/package.json @@ -0,0 +1,15 @@ +{ + "name": "copland-fossil-skin", + "version": "0.1.0", + "private": true, + "description": "A SourceHut-inspired skin for Fossil SCM", + "scripts": { + "build": "sass --no-source-map --style=expanded src/copland.scss ../../skins/copland/css.txt && node scripts/build-config.mjs", + "test:visual": "playwright test" + }, + "devDependencies": { + "@axe-core/playwright": "4.13.0", + "@playwright/test": "1.61.1", + "sass": "1.103.0" + } +} diff --git a/tools/copland/playwright.config.mjs b/tools/copland/playwright.config.mjs new file mode 100644 index 0000000..979000a --- /dev/null +++ b/tools/copland/playwright.config.mjs @@ -0,0 +1,20 @@ +import { defineConfig } from "@playwright/test"; + +export default defineConfig({ + testDir: "./tests", + testMatch: "visual.spec.mjs", + outputDir: "test-output", + reporter: [["list"], ["html", { open: "never" }]], + use: { + baseURL: "http://127.0.0.1:18082", + browserName: "chromium", + trace: "retain-on-failure", + }, + webServer: { + command: "COPLAND_TEST_PORT=18082 ./tests/serve-fixture.sh", + url: "http://127.0.0.1:18082/", + reuseExistingServer: false, + timeout: 30_000, + }, +}); + diff --git a/tools/copland/scripts/build-config.mjs b/tools/copland/scripts/build-config.mjs new file mode 100644 index 0000000..0a24317 --- /dev/null +++ b/tools/copland/scripts/build-config.mjs @@ -0,0 +1,30 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const skinDir = resolve(projectRoot, "../../skins/copland"); +const outputPath = resolve(projectRoot, "dist/copland-skin.config"); +const parts = ["css", "header", "footer", "details", "js"]; + +function fossilQuote(value) { + return `'${value.replaceAll("'", "''")}'`; +} + +const records = []; +for (const name of parts) { + const value = await readFile(resolve(skinDir, `${name}.txt`), "utf8"); + const record = `1 ${fossilQuote(name)} value ${fossilQuote(value)}`; + records.push(`config /config ${Buffer.byteLength(record, "utf8")}\n${record}\n`); +} + +const header = [ + "# Copland Fossil skin configuration", + "# Generated deterministically by tools/copland/scripts/build-config.mjs.", + "# Import with: fossil configuration import dist/copland-skin.config -R REPOSITORY", + "", +].join("\n"); + +await mkdir(dirname(outputPath), { recursive: true }); +await writeFile(outputPath, header + records.join(""), "utf8"); +console.log(`Wrote ${outputPath}`); diff --git a/tools/copland/scripts/create-demo.sh b/tools/copland/scripts/create-demo.sh new file mode 100755 index 0000000..7027e48 --- /dev/null +++ b/tools/copland/scripts/create-demo.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +set -euo pipefail + +root_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +source "$root_dir/scripts/fossil-env.sh" + +if [[ $# -ne 1 ]]; then + printf 'Usage: %s TARGET_DIRECTORY\n' "$0" >&2 + exit 2 +fi + +target_dir=$1 +mkdir -p "$target_dir/repositories" "$target_dir/checkouts" + +create_repository() { + local slug=$1 + local title=$2 + local repository="$target_dir/repositories/$slug.fossil" + local checkout="$target_dir/checkouts/$slug" + + local is_new=0 + if [[ ! -f "$repository" ]]; then + is_new=1 + "$FOSSIL" init \ + --admin-user copland \ + --date-override 2024-01-01T12:00:00Z \ + --project-name "$title" \ + "$repository" >/dev/null + fi + "$FOSSIL" user default copland -R "$repository" + mkdir -p "$checkout" + if [[ ! -f "$checkout/.fslckout" && ! -f "$checkout/_FOSSIL_" ]]; then + ( + cd /tmp + "$FOSSIL" open --nested --workdir "$checkout" "$repository" >/dev/null + ) + fi + ( + cd "$checkout" + "$FOSSIL" user default copland + if [[ "$is_new" -eq 1 ]]; then + mkdir -p src + printf '#+title: %s\n\nA small repository used to preview the Copland Fossil skin.\n\n* Usage\n\nRun the example from a C compiler.\n' "$title" > README.org + printf '#include <stdio.h>\n\nint main(void) {\n puts("hello from %s");\n return 0;\n}\n' "$slug" > src/main.c + fi + if [[ ! -f docs/org-support.org ]]; then + mkdir -p docs + printf '%s\n' \ + '#+title: Native Org preview' \ + '#+author: Copland' \ + '#+date: 2026-08-21' \ + '#+options: toc:2' \ + '' \ + '* Overview' \ + '' \ + 'This public fixture demonstrates *bold*, /italic/, _underlined_,' \ + '+struck+, ~code~, and =verbatim= text.' \ + '' \ + '- Rendered by Fossil' \ + ' 1. Nested ordered item' \ + '- [[file:../README.org][Back to the README]]' \ + '' \ + '* Data' \ + '' \ + '| Format | Native |' \ + '|--------+--------|' \ + '| Org | yes |' \ + '' \ + '#+begin_src nix' \ + '{ pkgs ? import <nixpkgs> {} }: pkgs.hello' \ + '#+end_src' \ + '' \ + '#+begin_example' \ + '<script>alert("escaped")</script>' \ + '#+end_example' \ + '' \ + '** Details' \ + '' \ + 'Jump [[*Overview][back to the overview]].' > docs/org-support.org + fi + "$FOSSIL" addremove >/dev/null + if [[ -n $("$FOSSIL" changes) ]]; then + "$FOSSIL" commit --no-warnings \ + --date-override 2024-01-03T12:00:00Z \ + --user-override copland \ + -m "Add Org documentation fixture" + fi + if [[ "$is_new" -eq 1 ]]; then + "$FOSSIL" tag add release-0.1 current + fi + ) + + local wiki_fixture="$target_dir/$slug-org-wiki.org" + printf '%s\n' \ + '#+TITLE: This title is intentionally suppressed' \ + '#+AUTHOR: Copland' \ + '#+OPTIONS: toc:2' \ + '' \ + '* Overview' \ + '' \ + 'Open [[wiki:Deployment Guide][the deployment guide]] or the' \ + '[[file:docs/org-support.org][repository documentation]].' \ + '' \ + '** Details' \ + '' \ + 'Return [[*Overview][to the overview]].' >"$wiki_fixture" + if ! "$FOSSIL" wiki list -R "$repository" | grep -Fxq 'Org Wiki'; then + "$FOSSIL" wiki create 'Org Wiki' "$wiki_fixture" -R "$repository" >/dev/null + fi +} + +create_repository alpha "Alpha" +create_repository beta "Beta" diff --git a/tools/copland/scripts/fossil-env.sh b/tools/copland/scripts/fossil-env.sh new file mode 100755 index 0000000..4df9a7b --- /dev/null +++ b/tools/copland/scripts/fossil-env.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +if [[ -z "${FOSSIL:-}" ]]; then + FOSSIL=fossil +fi +export FOSSIL + +fossil_binary_path() { + if [[ "$FOSSIL" == */* ]]; then + if [[ ! -x "$FOSSIL" ]]; then + printf 'Fossil binary is not executable: %s\n' "$FOSSIL" >&2 + return 1 + fi + (CDPATH= cd -- "$(dirname -- "$FOSSIL")" && printf '%s/%s\n' "$PWD" "$(basename -- "$FOSSIL")") + else + command -v "$FOSSIL" + fi +} + +require_org_fossil() { + local binary_path + if ! binary_path=$(fossil_binary_path); then + printf 'Fossil was not found. Run `make fossil` or set FOSSIL=/path/to/fossil.\n' >&2 + return 1 + fi + if ! "$FOSSIL" help test-org-render >/dev/null 2>&1; then + printf 'Fossil at %s has no native Org renderer. Run `make fossil` or set FOSSIL to the patched binary.\n' "$binary_path" >&2 + return 1 + fi + printf 'Fossil binary: %s\n' "$binary_path" +} + +verify_org_mime() { + local repository=$1 + local mime_result + mime_result=$("$FOSSIL" test-mimetype -R "$repository" probe.org) + if [[ "$mime_result" != *"text/x-org"* ]]; then + printf 'Selected Fossil does not map .org to text/x-org: %s\n' "$mime_result" >&2 + return 1 + fi + printf 'Org MIME check: text/x-org\n' +} diff --git a/tools/copland/scripts/install-repolist.sh b/tools/copland/scripts/install-repolist.sh new file mode 100755 index 0000000..d59f688 --- /dev/null +++ b/tools/copland/scripts/install-repolist.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail + +root_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +source "$root_dir/scripts/fossil-env.sh" +config_file="$root_dir/dist/copland-skin.config" + +if [[ $# -ne 1 ]]; then + printf 'Usage: %s REPOSITORY_DIRECTORY\n' "$0" >&2 + exit 2 +fi + +repository_dir=$1 +control_repository="$repository_dir/00-copland-skin.fossil" + +if [[ ! -d "$repository_dir" ]]; then + printf 'Repository directory does not exist: %s\n' "$repository_dir" >&2 + exit 1 +fi +if [[ ! -f "$config_file" ]]; then + printf 'Built skin not found. Run make build first.\n' >&2 + exit 1 +fi + +if [[ ! -f "$control_repository" ]]; then + "$FOSSIL" init "$control_repository" >/dev/null +fi + +"$FOSSIL" configuration import "$config_file" -R "$control_repository" +"$FOSSIL" settings repolist-skin 2 --exact -R "$control_repository" +"$FOSSIL" sql -R "$control_repository" \ + "REPLACE INTO config(name,value,mtime) VALUES('project-name','Copland',unixepoch());" + +printf 'Configured hidden repository-list skin: %s\n' "$control_repository" diff --git a/tools/copland/scripts/install.sh b/tools/copland/scripts/install.sh new file mode 100755 index 0000000..7fe0e1c --- /dev/null +++ b/tools/copland/scripts/install.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +root_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +source "$root_dir/scripts/fossil-env.sh" +config_file="$root_dir/dist/copland-skin.config" + +if [[ $# -ne 1 ]]; then + printf 'Usage: %s REPOSITORY\n' "$0" >&2 + exit 2 +fi + +repository=$1 +if [[ ! -f "$repository" ]]; then + printf 'Repository does not exist: %s\n' "$repository" >&2 + exit 1 +fi +if [[ ! -f "$config_file" ]]; then + printf 'Built skin not found. Run make build first.\n' >&2 + exit 1 +fi + +"$FOSSIL" configuration import "$config_file" -R "$repository" +printf 'Installed Copland into %s\n' "$repository" diff --git a/tools/copland/scripts/preview.sh b/tools/copland/scripts/preview.sh new file mode 100755 index 0000000..c82bec8 --- /dev/null +++ b/tools/copland/scripts/preview.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +root_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +source_root=$(CDPATH= cd -- "$root_dir/../.." && pwd) +source "$root_dir/scripts/fossil-env.sh" +preview_dir="$source_root/.copland-preview" +nix_repository="$source_root/.repository/nix.fossil" +port=${PORT:-8080} + +require_org_fossil +"$root_dir/scripts/create-demo.sh" "$preview_dir" +verify_org_mime "$preview_dir/repositories/alpha.fossil" + +if [[ ! -f "$nix_repository" ]]; then + printf 'Nix documentation repository not found: %s\n' "$nix_repository" >&2 + exit 1 +fi +ln -sfn "$nix_repository" "$preview_dir/repositories/nix.fossil" +verify_org_mime "$preview_dir/repositories/nix.fossil" + +printf 'Copland preview: http://127.0.0.1:%s/\n' "$port" +printf 'Nix wiki: http://127.0.0.1:%s/nix/wiki\n' "$port" +printf 'Press Ctrl-C to stop the server.\n' +FOSSIL_REPOLIST_TITLE=Copland exec "$FOSSIL" server \ + --localhost \ + --localauth \ + --port "$port" \ + --repolist \ + --skin copland \ + "$preview_dir/repositories" diff --git a/tools/copland/src/copland.scss b/tools/copland/src/copland.scss new file mode 100644 index 0000000..8c92ad5 --- /dev/null +++ b/tools/copland/src/copland.scss @@ -0,0 +1,443 @@ +$bp-sm: 36rem; +$bp-md: 48rem; +$bp-lg: 62rem; +$bp-xl: 75rem; + +:root { + color-scheme: light dark; + --page-bg: #fff; + --surface: #fff; + --surface-subtle: #f5f5f5; + --surface-muted: #e9ecef; + --surface-active: #ddd; + --text: #212529; + --text-muted: #6c757d; + --text-faint: #767676; + --border: #ced4da; + --border-strong: #888; + --link: #0645d9; + --link-hover: #002f9f; + --primary: #0056d6; + --primary-hover: #0044aa; + --primary-text: #fff; + --danger: #b00020; + --danger-bg: #f8d7da; + --success: #146c2e; + --success-bg: #d4edda; + --warning-bg: #fff3cd; + --info-bg: #d1ecf1; + --code-bg: #f1f3f5; + --selection: #b9d6ff; + --focus: #ffbf47; + --shadow: rgba(0, 0, 0, 0.12); + --container: 71.25rem; + --gutter: 1rem; + --radius: 0; + --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; +} + +@media (prefers-color-scheme: dark) { + :root { + --page-bg: #212529; + --surface: #212529; + --surface-subtle: #191c1f; + --surface-muted: #2d3237; + --surface-active: #3b4147; + --text: #f1f3f5; + --text-muted: #c2c7cc; + --text-faint: #adb5bd; + --border: #565e65; + --border-strong: #8b949c; + --link: #5c9cff; + --link-hover: #91bcff; + --primary: #4d8ee8; + --primary-hover: #70a7f2; + --primary-text: #101214; + --danger: #ff6b78; + --danger-bg: #57242a; + --success: #65d47d; + --success-bg: #183d24; + --warning-bg: #514318; + --info-bg: #193e47; + --code-bg: #17191b; + --selection: #173f70; + --focus: #ffd166; + --shadow: rgba(0, 0, 0, 0.4); + } +} + +* { box-sizing: border-box; } + +html { + min-height: 100%; + scrollbar-gutter: stable; + font-size: 16px; +} + +body { + min-height: 100vh; + margin: 0; + padding: 0 0 1rem; + background: var(--page-bg); + color: var(--text); + font-family: var(--font-sans); + font-size: 1rem; + line-height: 1.5; + text-align: initial; +} + +::selection { background: var(--selection); } + +.container, +div.content { + width: 100%; + max-width: var(--container); + margin-inline: auto; + padding-inline: var(--gutter); +} + +div.content { min-height: 45vh; } + +body a { + color: var(--link); + text-decoration: underline; + text-underline-offset: 0.12em; +} + +body a:hover { color: var(--link-hover); text-decoration: none; } +a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible, +textarea:focus-visible, [tabindex]:focus-visible { + outline: 3px solid var(--focus); + outline-offset: 2px; +} + +h1, h2, h3, h4, h5, h6 { + margin: 1rem 0 0.5rem; + color: var(--text); + font-weight: 500; + line-height: 1.2; +} + +h1 { font-size: 1.75rem; } +h2 { font-size: 1.45rem; } +h3 { font-size: 1.15rem; border-bottom: 1px solid var(--border); } +h4 { font-size: 1rem; border-bottom: 1px solid var(--border); } +p { margin: 0 0 1rem; } +hr { border: 0; border-top: 1px solid var(--border); } + +.site-header { margin-bottom: 0; } +.site-header-row { + min-height: 3.25rem; + display: flex; + align-items: center; + gap: 1rem; +} + +.site-brand, +.resource-title a, +.account-nav a, +.mainmenu a { + text-decoration: none; +} + +.site-brand { + flex: 0 0 auto; + color: var(--text); + font-size: 1.25rem; + font-weight: 600; + letter-spacing: -0.025em; +} + +.site-brand::before { + content: ""; + display: inline-block; + width: 0.78rem; + height: 0.78rem; + margin-right: 0.42rem; + border: 2px solid currentColor; + border-radius: 50%; + vertical-align: 0.02rem; +} + +.site-brand:hover, +.resource-title a:hover, +.account-nav a:hover, +.mainmenu a:hover { color: var(--text); text-decoration: underline; } + +.resource-title { + min-width: 0; + display: flex; + align-items: baseline; + gap: 0.4rem; + overflow: hidden; + white-space: nowrap; +} + +.resource-title a, +.resource-page { + overflow: hidden; + text-overflow: ellipsis; +} + +.resource-title a { color: var(--text); font-weight: 600; } +.resource-separator, .resource-page { color: var(--text-muted); } +.account-nav { margin-left: auto; white-space: nowrap; font-size: 0.9rem; } + +.resource-nav { + border-bottom: 3px solid var(--surface-active); +} + +.mainmenu { + min-height: 2rem; + display: flex; + align-items: end; + gap: 0; + overflow-x: auto; + scrollbar-width: thin; +} + +.mainmenu a { + flex: 0 0 auto; + padding: 0.25rem 0.85rem 0.18rem; + color: var(--text-muted); + white-space: nowrap; +} + +.mainmenu a.active, +.mainmenu a.selected, +.mainmenu a:hover { + color: var(--text); +} + +.mainmenu a.active, +.mainmenu a.selected { background: var(--surface-active); } + +.page-title { + margin-top: 0.9rem; + margin-bottom: 0.75rem; + font-size: 1.35rem; + font-weight: 500; +} + +.site-footer { + margin-top: 2rem; + color: var(--text-muted); + font-size: 0.8rem; +} +.site-footer .container { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.4rem; } + +pre, code, kbd, samp, tt { font-family: var(--font-mono); } +code, tt { overflow-wrap: anywhere; } +pre { + max-width: 100%; + overflow: auto; + margin: 0 0 1rem; + padding: 0.75rem 1rem; + background: var(--code-bg); + color: var(--text); + font-size: 0.875rem; + line-height: 1.35; + tab-size: 4; +} +pre a { color: inherit; } +blockquote { margin: 0 0 1rem; padding-left: 1rem; border-left: 4px solid var(--border); } + +.org-document { + max-width: 100%; +} + +.org-document .org-title { margin-top: 0; } + +.org-metadata { + display: grid; + grid-template-columns: max-content minmax(0, 1fr); + gap: 0.15rem 0.75rem; + margin: 0 0 1rem; + color: var(--text-muted); + font-size: 0.92rem; +} + +.org-metadata dt { font-weight: 600; } +.org-metadata dd { margin: 0; } + +.org-toc { + margin: 0 0 1.25rem; + padding: 0.75rem 1rem; + border: 1px solid var(--border); + background: var(--surface-subtle); +} + +.org-toc h2 { margin-top: 0; font-size: 1.05rem; } +.org-toc ol { margin: 0; padding-left: 1.5rem; } +.org-toc-level-2 { margin-left: 1rem; } +.org-toc-level-3 { margin-left: 2rem; } +.org-toc-level-4, .org-toc-level-5, .org-toc-level-6 { margin-left: 3rem; } + +.org-table { display: block; overflow-x: auto; } +.org-block { border-left: 3px solid var(--border-strong); } +.org-example { color: var(--text); } +.org-unknown-block { border-left-color: var(--danger); } + +table { + width: 100%; + margin-bottom: 1rem; + border-collapse: collapse; + color: var(--text); +} +th { text-align: left; font-weight: 600; } +th, td { padding: 0.28rem 0.5rem; vertical-align: top; border-top: 1px solid var(--border); } +thead th { border-bottom: 2px solid var(--border-strong); } +tbody tr:nth-child(odd) { background: color-mix(in srgb, var(--surface-subtle) 70%, transparent); } +table.browser, table.report, table.timelineTable { font-size: 0.92rem; } +table.browser td:first-child, table.browser td:nth-child(2) { font-family: var(--font-mono); } + +input, button, select, textarea { font: inherit; } +input[type="text"], input[type="search"], input[type="email"], input[type="password"], +input[type="url"], input[type="number"], textarea, select { + max-width: 100%; + padding: 0.35rem 0.5rem; + border: 1px solid var(--border-strong); + border-radius: var(--radius); + background: var(--surface); + color: var(--text); +} +textarea { width: 100%; } +label { display: inline-block; margin-bottom: 0.25rem; } +button, input[type="submit"], input[type="button"], .button, a.button { + display: inline-block; + padding: 0.35rem 0.7rem; + border: 1px solid var(--primary); + border-radius: var(--radius); + background: var(--primary); + color: var(--primary-text); + cursor: pointer; + line-height: 1.25; + text-decoration: none; +} +button:hover, input[type="submit"]:hover, input[type="button"]:hover, .button:hover { + border-color: var(--primary-hover); + background: var(--primary-hover); +} +button:disabled, input:disabled { cursor: not-allowed; opacity: 0.65; } +fieldset { margin: 0 0 1rem; border: 1px solid var(--border); } +legend { padding-inline: 0.35rem; } + +.submenu, .sectionmenu { + margin: 0 0 1rem; + padding: 0.4rem 0.6rem; + background: var(--surface-subtle); + border: 1px solid var(--border); +} +.submenu a, .sectionmenu a { margin-right: 0.75rem; } + +.generalError, .error, .fatalError, .loginError, +.warning, .notice, .success { + margin: 0.75rem 0; + padding: 0.65rem 0.8rem; + border: 1px solid var(--border); +} +.generalError, .error, .fatalError, .loginError { background: var(--danger-bg); color: var(--text); } +.warning { background: var(--warning-bg); } +.notice { background: var(--info-bg); } +.success { background: var(--success-bg); } + +.timelineDate { margin-top: 1rem; color: var(--text); font-weight: 600; } +.timelineHistDsp { color: var(--text-muted); } +.timelineModernCell, .timelineColumnarCell, .timelineDetailCell { padding-block: 0.35rem; } +.timelineModernDetail { opacity: 1; } +.timelineCompactComment, .timelineComment { line-height: 1.35; } +.timelineTime { color: var(--text-muted); white-space: nowrap; } +.timelineGraph { min-width: 2rem; } +.tl-node { border-radius: 50%; } +.timelineModernCell a, +.timelineColumnarCell a, +.timelineDetailCell a { color: var(--link) !important; } +.timelineModernCell a:hover, +.timelineColumnarCell a:hover, +.timelineDetailCell a:hover { color: var(--link-hover) !important; } + +.filetree { font-family: var(--font-mono); font-size: 0.9rem; } +.filetree ul { padding-left: 1.25rem; } +.filetree a { color: var(--text); } +.filetree a:hover { color: var(--link); } +.fileage tr:hover, table.browser tr:hover { background: var(--surface-active); } + +.diff, .udiff, .sbsdiff { font-family: var(--font-mono); font-size: 0.82rem; } +.diff pre, .udiff pre, .sbsdiff pre { background: transparent; } +.diffln, .diffhr { color: var(--text-muted); background: var(--surface-muted); } +.diffadd, .diffins { background: var(--success-bg); } +.diffrm, .diffdel { background: var(--danger-bg); } +.diffchng { background: var(--warning-bg); } +.selectedText { background: var(--selection); } + +.forumPost, .forumTime, .forumSel, .forumHierRoot, .forumHier { + border-color: var(--border); +} +.forumPost, .forumTime { background: var(--surface-subtle); color: var(--text); } +.forumPostBody { overflow-wrap: anywhere; } +.forumPostBody img, .wiki-content img, .markdown img { max-width: 100%; height: auto; } + +.ticketDspLabel, .ticketDspValue { padding: 0.35rem 0.5rem; } +.ticketDspLabel { color: var(--text-muted); font-weight: 600; } +.ticketDspValue { border-bottom: 1px solid var(--border); } + +.branchname, .tagname, .wikiTagCancelled, .infoTagCancelled { + display: inline-block; + padding: 0.05rem 0.3rem; + background: var(--surface-muted); + color: var(--text); + font-size: 0.82rem; +} + +.statistics-report-table-events th, .statistics-report-table-events td { text-align: right; } +.statistics-report-table-events th:first-child, .statistics-report-table-events td:first-child { text-align: left; } +.accordion_panel { border-color: var(--border); background: var(--surface); } +.accordion_marker { color: var(--text-muted); } + +body.repolist .resource-title, +body.repolist .resource-nav, +body.repolist > .page-title { display: none; } +body.repolist div.content { padding-top: 1rem; } +body.repolist table td:first-child { font-family: var(--font-mono); font-weight: 600; } + +@media (prefers-color-scheme: dark) { + img { opacity: 0.92; } + body .timelineModernCell, + body .timelineColumnarCell, + body .timelineDetailCell, + .timelineModernCell[style*="background-color"], + .timelineColumnarCell[style*="background-color"], + .timelineDetailCell[style*="background-color"], + td[style*="background-color"] { + background-color: var(--surface-subtle) !important; + color: var(--text) !important; + } + .diffadd, .diffins { color: var(--text); } + .diffrm, .diffdel { color: var(--text); } +} + +@media (max-width: $bp-md) { + :root { --gutter: 0.75rem; } + .site-header-row { min-height: 2.8rem; gap: 0.65rem; } + .resource-page, .resource-separator { display: none; } + .mainmenu a { padding-inline: 0.65rem; } + .page-title { font-size: 1.2rem; } + table:not(.sbsdiffcols) { display: block; max-width: 100%; overflow-x: auto; } + .desktoponly { display: none !important; } +} + +@media (max-width: $bp-sm) { + .site-brand { font-size: 1.1rem; } + .resource-title { font-size: 0.9rem; } + .account-nav { font-size: 0.82rem; } + .page-title { margin-top: 0.7rem; } + pre { padding: 0.6rem 0.7rem; } +} + +@media print { + .resource-nav, .account-nav, .submenu, .sectionmenu, .site-footer { display: none !important; } + body, pre, code { background: #fff !important; color: #000 !important; } + a { color: #000; text-decoration: underline; } + div.content, .container { max-width: none; padding: 0; } +} diff --git a/tools/copland/tests/fixtures/org/all.html b/tools/copland/tests/fixtures/org/all.html new file mode 100644 index 0000000..33fc537 --- /dev/null +++ b/tools/copland/tests/fixtures/org/all.html @@ -0,0 +1,50 @@ +<div class="org-document"> +<h1 class="org-title">Über & <Fossil></h1> +<dl class="org-metadata"> +<dt>Author</dt><dd>Ada <admin@example.test></dd> +<dt>Date</dt><dd>2026-08-21</dd> +</dl> +<nav class="org-toc" aria-label="Table of contents"><h2>Contents</h2> +<ol> +<li class="org-toc-level-1"><a href="#intro-safety">Intro & safety</a></li> +<li class="org-toc-level-2"><a href="#duplicate">Duplicate</a></li> +<li class="org-toc-level-1"><a href="#duplicate-2">Duplicate</a></li> +<li class="org-toc-level-1"><a href="#日本語">日本語</a></li> +</ol> +</nav> +<h1 id="intro-safety">Intro & safety</h1> +<p>A paragraph with <strong>bold</strong>, <em>italic</em>, <u>underline</u>, <del>strike</del>, <code><code></code>, and <code>verbatim & text</code>. Words_like_this and paths/like/this stay literal.</p> +<ul> +<li>first<ul> +<li>nested <strong>item</strong></li> +</ul> +<ol> +<li>nested ordered</li> +</ol> +</li> +<li>second</li> +</ul> +<ol> +<li>one</li> +<li>two</li> +</ol> +<table class="org-table"> +<thead> +<tr><th>Name</th><th>Value</th></tr> +</thead> +<tbody> +<tr><td>α</td><td><tag></td></tr> +<tr><td>two</td><td><strong>bold</strong></td></tr> +</tbody> +</table> +<pre class="org-block org-src has-language"><code class="language-nix">{ pkgs, ... }: <unsafe> & value</code></pre> +<pre class="org-block org-example"><code>example <script>alert(1)</script></code></pre> +<p><a href="https://example.test/a?x=1&y=2">external</a> <a href="guide.org">relative</a> <a href="#intro-safety">internal</a> unsafe</p> +<h2 id="duplicate">Duplicate</h2> +<p>Text.</p> +<h1 id="duplicate-2">Duplicate</h1> +<p>More.</p> +<h1 id="日本語">日本語</h1> +<p>Unicode heading.</p> +</div> + diff --git a/tools/copland/tests/fixtures/org/all.org b/tools/copland/tests/fixtures/org/all.org new file mode 100644 index 0000000..f5c644a --- /dev/null +++ b/tools/copland/tests/fixtures/org/all.org @@ -0,0 +1,52 @@ +#+TITLE: Über & <Fossil> +#+AUTHOR: Ada <admin@example.test> +#+DATE: 2026-08-21 +#+OPTIONS: toc:2 num:nil +#+STARTUP: overview + +:PROPERTIES: +:ID: hidden +:END: + +* Intro & safety + +A paragraph with *bold*, /italic/, _underline_, +strike+, ~<code>~, and +=verbatim & text=. Words_like_this and paths/like/this stay literal. + +- first + - nested *item* + 1. nested ordered +- second + +1. one +2. two + +| Name | Value | +|------+-------| +| α | <tag> | +| two | *bold* | + +#+begin_src nix +{ pkgs, ... }: <unsafe> & value +#+end_src + +#+begin_example +example <script>alert(1)</script> +#+end_example + +[[https://example.test/a?x=1&y=2][external]] +[[file:guide.org][relative]] +[[*Intro & safety][internal]] +[[javascript:alert(1)][unsafe]] + +** Duplicate + +Text. + +* Duplicate + +More. + +* 日本語 + +Unicode heading. diff --git a/tools/copland/tests/fixtures/org/all.txt b/tools/copland/tests/fixtures/org/all.txt new file mode 100644 index 0000000..266ab90 --- /dev/null +++ b/tools/copland/tests/fixtures/org/all.txt @@ -0,0 +1,66 @@ +Über & <Fossil> + + + +AuthorAda <admin@example.test> +Date2026-08-21 + + + +Contents + +Intro & safety +Duplicate +Duplicate +日本語 + + + +Intro & safety + +A paragraph with bold, italic, underline, strike, <code>, and verbatim & text. Words_like_this and paths/like/this stay literal. + +first +nested item + + +nested ordered + + +second + + +one +two + + + + + +NameValue + + + +α<tag> +twobold + + + +{ pkgs, ... }: <unsafe> & value + +example <script>alert(1)</script> + +external relative internal unsafe + +Duplicate + +Text. + +Duplicate + +More. + +日本語 + +Unicode heading. + diff --git a/tools/copland/tests/fixtures/org/security.html b/tools/copland/tests/fixtures/org/security.html new file mode 100644 index 0000000..38707e0 --- /dev/null +++ b/tools/copland/tests/fixtures/org/security.html @@ -0,0 +1,9 @@ +<div class="org-document"> +<h1 class="org-title"><img src=x onerror=alert(1)></h1> +<p>Raw <script>alert(1)</script> and "quotes".</p> +<p><a href="https://example.test/" onmouseover="alert(1)">attribute</a> file scheme confusion</p> +<pre class="org-block org-unknown-block"><code><img src=x onerror=alert(1)></code></pre> +<pre class="org-block org-src has-language"><code class="language-sh">echo "unterminated and escaped <tag>" +</code></pre> +</div> + diff --git a/tools/copland/tests/fixtures/org/security.org b/tools/copland/tests/fixtures/org/security.org new file mode 100644 index 0000000..3e7280a --- /dev/null +++ b/tools/copland/tests/fixtures/org/security.org @@ -0,0 +1,13 @@ +#+TITLE: <img src=x onerror=alert(1)> + +Raw <script>alert(1)</script> and "quotes". + +[[https://example.test/" onmouseover="alert(1)][attribute]] +[[file:javascript:alert(1)][file scheme confusion]] + +#+begin_export html +<img src=x onerror=alert(1)> +#+end_export + +#+begin_src sh +echo "unterminated and escaped <tag>" diff --git a/tools/copland/tests/org-renderer.sh b/tools/copland/tests/org-renderer.sh new file mode 100755 index 0000000..1822d52 --- /dev/null +++ b/tools/copland/tests/org-renderer.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +root_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +source "$root_dir/scripts/fossil-env.sh" +fixture_dir="$root_dir/tests/fixtures/org" +output_dir=$(mktemp -d -t copland-org-renderer.XXXXXX) + +for fixture in all security; do + "$FOSSIL" test-org-render "$fixture_dir/$fixture.org" >"$output_dir/$fixture.html" + diff -u "$fixture_dir/$fixture.html" "$output_dir/$fixture.html" +done + +"$FOSSIL" test-org-render --text "$fixture_dir/all.org" >"$output_dir/all.txt" +diff -u "$fixture_dir/all.txt" "$output_dir/all.txt" + +sed 's/$/\r/' "$fixture_dir/all.org" >"$output_dir/all-crlf.org" +"$FOSSIL" test-org-render "$output_dir/all-crlf.org" >"$output_dir/all-crlf.html" +diff -u "$fixture_dir/all.html" "$output_dir/all-crlf.html" + +if rg -n '<script|<img[^>]*onerror|href="javascript:' "$output_dir"/*.html; then + printf 'Unsafe active HTML escaped into Org renderer output.\n' >&2 + exit 1 +fi +if ! rg -q '<script>|<img' "$output_dir/security.html"; then + printf 'Escaped security fixture content is missing.\n' >&2 + exit 1 +fi + +printf 'Org renderer golden and security tests passed.\n' diff --git a/tools/copland/tests/serve-fixture.sh b/tools/copland/tests/serve-fixture.sh new file mode 100755 index 0000000..7f2bc8d --- /dev/null +++ b/tools/copland/tests/serve-fixture.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +root_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +source "$root_dir/scripts/fossil-env.sh" +fixture_dir=$(mktemp -d -t copland-visual.XXXXXX) +port=${COPLAND_TEST_PORT:-18082} + +require_org_fossil +"$root_dir/scripts/create-demo.sh" "$fixture_dir" +verify_org_mime "$fixture_dir/repositories/alpha.fossil" + +FOSSIL_REPOLIST_TITLE=Copland exec "$FOSSIL" server \ + --localhost \ + --localauth \ + --port "$port" \ + --repolist \ + "$fixture_dir/repositories" diff --git a/tools/copland/tests/smoke.sh b/tools/copland/tests/smoke.sh new file mode 100755 index 0000000..bd54bab --- /dev/null +++ b/tools/copland/tests/smoke.sh @@ -0,0 +1,188 @@ +#!/usr/bin/env bash +set -euo pipefail + +root_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +source "$root_dir/scripts/fossil-env.sh" +test_dir=$(mktemp -d -t copland-smoke.XXXXXX) +port=${COPLAND_TEST_PORT:-18081} +server_pid="" + +finish() { + if [[ -n "$server_pid" ]]; then + kill "$server_pid" 2>/dev/null || true + wait "$server_pid" 2>/dev/null || true + fi +} +trap finish EXIT INT TERM + +require_org_fossil +"$root_dir/scripts/create-demo.sh" "$test_dir" +verify_org_mime "$test_dir/repositories/alpha.fossil" +"$FOSSIL" sql -R "$test_dir/repositories/alpha.fossil" \ + "REPLACE INTO config(name,value,mtime) VALUES('doc-glob','*.org',unixepoch());" +"$FOSSIL" fts-config enable document -R "$test_dir/repositories/alpha.fossil" >/dev/null +"$FOSSIL" fts-config enable wiki -R "$test_dir/repositories/alpha.fossil" >/dev/null +"$FOSSIL" fts-config index on -R "$test_dir/repositories/alpha.fossil" >/dev/null +"$FOSSIL" fts-config reindex -R "$test_dir/repositories/alpha.fossil" >/dev/null + +FOSSIL_REPOLIST_TITLE=Copland "$FOSSIL" server \ + --localhost \ + --localauth \ + --port "$port" \ + --repolist \ + "$test_dir/repositories" >"$test_dir/server.log" 2>&1 & +server_pid=$! + +base_url="http://127.0.0.1:$port" +for attempt in $(seq 1 50); do + if curl --fail --location --silent "$base_url/" >/dev/null; then + break + fi + if [[ "$attempt" -eq 50 ]]; then + printf 'Fossil server did not start.\n' >&2 + sed -n '1,120p' "$test_dir/server.log" >&2 + exit 1 + fi + sleep 0.1 +done + +paths=( + "/" + "/alpha/home" + "/alpha/timeline" + "/alpha/tree?ci=tip" + "/alpha/dir?ci=tip" + "/alpha/brlist" + "/alpha/taglist" + "/alpha/wiki" + "/alpha/wiki?name=Org%20Wiki" + "/alpha/wikiedit?name=New%20Org%20Page" + "/alpha/whistory?name=Org%20Wiki" + "/alpha/reportlist" + "/alpha/forum" + "/alpha/login" + "/alpha/sitemap" + "/alpha/doc/tip/docs/org-support.org" + "/alpha/file?name=docs/org-support.org&ci=tip" + "/alpha/file?name=docs/org-support.org&ci=tip&txt=1" + "/alpha/search?s=Native%20Org%20preview&y=d" + "/alpha/search?s=deployment%20guide&y=w" +) + +for path in "${paths[@]}"; do + body=$(curl --fail --location --silent --show-error "$base_url$path") + if [[ "$body" != *"Copland"* ]]; then + printf 'Copland marker missing from %s\n' "$path" >&2 + exit 1 + fi + if [[ "$body" == *"thmainError"* ]]; then + printf 'TH1 template error on %s\n' "$path" >&2 + exit 1 + fi + if [[ "$path" == "/" && ("$body" != *"Alpha"* || "$body" != *"Beta"*) ]]; then + printf 'Demo repositories missing from repository list\n' >&2 + exit 1 + fi + if [[ "$path" == "/alpha/home" ]]; then + mainmenu=${body#*'<nav class="container mainmenu"'} + mainmenu=${mainmenu%%'</nav>'*} + for hidden_route in chat taglist forum reportlist sitemap ticket tkt; do + if [[ "$mainmenu" == *"/$hidden_route"* ]]; then + printf 'Hidden route /%s is still present in the main menu\n' "$hidden_route" >&2 + exit 1 + fi + done + fi + if [[ "$path" == "/alpha/wiki" ]]; then + if [[ "$body" != *"Available Wiki Pages"* || "$body" != *"Org Wiki"* ]]; then + printf 'Wiki landing page did not list available pages\n' >&2 + exit 1 + fi + fi + if [[ "$path" == "/alpha/tree?ci=tip" ]]; then + if [[ "$body" != *'class="org-document"'* || "$body" != *"A small repository"* ]]; then + printf 'README.org was not rendered below the repository tree\n' >&2 + exit 1 + fi + fi + if [[ "$path" == "/alpha/doc/tip/docs/org-support.org" ]]; then + if [[ "$body" != *'<title>Alpha: Native Org preview</title>'* + || "$body" != *'class="org-toc"'* + || "$body" != *'href="../README.org"'* + || "$body" != *'id="overview"'* ]]; then + printf 'Native Org /doc rendering is incomplete\n' >&2 + exit 1 + fi + fi + if [[ "$path" == "/alpha/wiki?name=Org%20Wiki" ]]; then + if [[ "$body" != *'<title>Alpha: Org Wiki</title>'* + || "$body" != *'class="org-document"'* + || "$body" == *'class="org-title"'* + || "$body" != *'href="wiki?name=Deployment+Guide"'* + || "$body" != *'href="doc/tip/docs/org-support.org"'* ]]; then + printf 'Native Org wiki rendering is incomplete\n' >&2 + exit 1 + fi + fi + if [[ "$path" == "/alpha/wikiedit?name=New%20Org%20Page" ]]; then + if [[ "$body" != *'value="text/x-org" selected'* ]]; then + printf 'Org is not the default format in the wiki editor\n' >&2 + exit 1 + fi + fi + if [[ "$path" == "/alpha/file?name=docs/org-support.org&ci=tip" ]]; then + if [[ "$body" != *'class="org-document"'* || "$body" != *'>Text</a>'* ]]; then + printf 'Native Org /file rendering or Text toggle is missing\n' >&2 + exit 1 + fi + fi + if [[ "$path" == "/alpha/file?name=docs/org-support.org&ci=tip&txt=1" ]]; then + if [[ "$body" == *'class="org-document"'* || "$body" != *'#+title: Native Org preview'* ]]; then + printf 'Org /file Text view did not show source\n' >&2 + exit 1 + fi + fi + if [[ "$path" == "/alpha/search?s=Native%20Org%20preview&y=d" ]]; then + if [[ "$body" != *"Native Org preview"* ]]; then + printf 'Org document was not found by full-text search\n' >&2 + exit 1 + fi + fi + if [[ "$path" == "/alpha/search?s=deployment%20guide&y=w" ]]; then + if [[ "$body" != *"Org Wiki"* ]]; then + printf 'Org wiki page was not found by full-text search\n' >&2 + exit 1 + fi + fi + printf 'ok %s\n' "$path" +done + +default_skin_body=$(curl --fail --location --silent --show-error \ + "$base_url/alpha/home?skin=default") +if [[ "$default_skin_body" == *'class="site-brand"'* ]]; then + printf 'The original Fossil default skin is not selectable\n' >&2 + exit 1 +fi +copland_skin_body=$(curl --fail --location --silent --show-error \ + "$base_url/alpha/home?skin=copland") +if [[ "$copland_skin_body" != *'class="site-brand"'* ]]; then + printf 'The built-in Copland skin is not selectable\n' >&2 + exit 1 +fi + +json_wiki=$(curl --fail --silent --show-error \ + "$base_url/alpha/json/wiki/get?name=Org%20Wiki&format=html") +if [[ "$json_wiki" != *'org-document'* || "$json_wiki" != *'contentFormat":"html'* ]]; then + printf 'JSON wiki output did not render Org\n' >&2 + exit 1 +fi +json_preview=$(curl --fail --silent --show-error \ + --header 'Content-Type: application/json' \ + --data '{"payload":{"body":"* JSON preview","mimetype":"text/x-org"}}' \ + "$base_url/alpha/json/wiki/preview") +if [[ "$json_preview" != *'org-document'* || "$json_preview" != *'JSON preview'* ]]; then + printf 'JSON wiki preview did not render Org\n' >&2 + exit 1 +fi + +printf 'Smoke tests passed. Temporary fixtures: %s\n' "$test_dir" diff --git a/tools/copland/tests/visual.spec.mjs b/tools/copland/tests/visual.spec.mjs new file mode 100644 index 0000000..73559e1 --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs @@ -0,0 +1,94 @@ +import AxeBuilder from "@axe-core/playwright"; +import { expect, test } from "@playwright/test"; + +const pages = [ + ["repository-list", "/"], + ["home", "/alpha/home"], + ["timeline", "/alpha/timeline"], + ["tree", "/alpha/tree?ci=tip"], + ["org-document", "/alpha/doc/tip/docs/org-support.org"], + ["org-wiki", "/alpha/wiki?name=Org%20Wiki"], + ["login", "/alpha/login"], +]; + +for (const colorScheme of ["light", "dark"]) { + for (const [name, path] of pages) { + test(`${name} is legible in ${colorScheme} mode`, async ({ page }) => { + await page.emulateMedia({ colorScheme }); + await page.goto(path); + await expect(page.locator("body")).toContainText("Copland"); + + const viewportOverflow = await page.evaluate( + () => document.documentElement.scrollWidth > document.documentElement.clientWidth, + ); + expect(viewportOverflow).toBe(false); + + let accessibilityScan = new AxeBuilder({ page }) + .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]) + .disableRules(["html-has-lang"]) + .exclude(".submenu"); + if (name === "login") { + accessibilityScan = accessibilityScan.exclude("form"); + } + const accessibility = await accessibilityScan.analyze(); + expect(accessibility.violations).toEqual([]); + + const screenshotMasks = [page.locator(".site-footer span:last-child")]; + if (name === "login") { + screenshotMasks.push(page.locator("form")); + } + await expect(page).toHaveScreenshot(`${name}-${colorScheme}.png`, { + fullPage: true, + animations: "disabled", + mask: screenshotMasks, + maxDiffPixelRatio: 0.001, + }); + }); + } +} + +test.describe("mobile", () => { + test.use({ viewport: { width: 320, height: 720 } }); + + test("navigation remains usable", async ({ page }) => { + await page.goto("/alpha/timeline"); + await expect(page.locator(".site-brand")).toHaveText("Copland"); + await expect(page.locator(".mainmenu")).toBeVisible(); + const viewportOverflow = await page.evaluate( + () => document.documentElement.scrollWidth > document.documentElement.clientWidth, + ); + expect(viewportOverflow).toBe(false); + await expect(page).toHaveScreenshot("timeline-mobile.png", { + fullPage: true, + mask: [page.locator(".site-footer span:last-child")], + }); + }); + + test("Org documentation remains readable", async ({ page }) => { + await page.goto("/alpha/doc/tip/docs/org-support.org"); + await expect(page.locator(".org-document")).toBeVisible(); + await expect(page.locator(".org-toc")).toBeVisible(); + const viewportOverflow = await page.evaluate( + () => document.documentElement.scrollWidth > document.documentElement.clientWidth, + ); + expect(viewportOverflow).toBe(false); + await expect(page).toHaveScreenshot("org-document-mobile.png", { + fullPage: true, + mask: [page.locator(".site-footer span:last-child")], + }); + }); + + test("Org wiki remains readable", async ({ page }) => { + await page.goto("/alpha/wiki?name=Org%20Wiki"); + await expect(page.locator(".org-document")).toBeVisible(); + await expect(page.locator(".org-toc")).toBeVisible(); + const viewportOverflow = await page.evaluate( + () => document.documentElement.scrollWidth > document.documentElement.clientWidth, + ); + expect(viewportOverflow).toBe(false); + await expect(page).toHaveScreenshot("org-wiki-mobile.png", { + fullPage: true, + mask: [page.locator(".site-footer span:last-child")], + }); + }); +}); diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/home-dark-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/home-dark-linux.png Binary files differnew file mode 100644 index 0000000..52f9478 --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/home-dark-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/home-light-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/home-light-linux.png Binary files differnew file mode 100644 index 0000000..4660bf9 --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/home-light-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/login-dark-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/login-dark-linux.png Binary files differnew file mode 100644 index 0000000..738588b --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/login-dark-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/login-light-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/login-light-linux.png Binary files differnew file mode 100644 index 0000000..03ba2db --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/login-light-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/org-document-dark-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/org-document-dark-linux.png Binary files differnew file mode 100644 index 0000000..15d37f9 --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/org-document-dark-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/org-document-light-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/org-document-light-linux.png Binary files differnew file mode 100644 index 0000000..3c0785f --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/org-document-light-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/org-document-mobile-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/org-document-mobile-linux.png Binary files differnew file mode 100644 index 0000000..d1f5cba --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/org-document-mobile-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-dark-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-dark-linux.png Binary files differnew file mode 100644 index 0000000..464197b --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-dark-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-light-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-light-linux.png Binary files differnew file mode 100644 index 0000000..ff3bdc1 --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-light-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-mobile-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-mobile-linux.png Binary files differnew file mode 100644 index 0000000..50381e0 --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-mobile-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/repository-list-dark-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/repository-list-dark-linux.png Binary files differnew file mode 100644 index 0000000..0cf1b4f --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/repository-list-dark-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/repository-list-light-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/repository-list-light-linux.png Binary files differnew file mode 100644 index 0000000..af49135 --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/repository-list-light-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/timeline-dark-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/timeline-dark-linux.png Binary files differnew file mode 100644 index 0000000..469d7b0 --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/timeline-dark-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/timeline-light-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/timeline-light-linux.png Binary files differnew file mode 100644 index 0000000..e1dbe69 --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/timeline-light-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/timeline-mobile-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/timeline-mobile-linux.png Binary files differnew file mode 100644 index 0000000..02ed528 --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/timeline-mobile-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/tree-dark-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/tree-dark-linux.png Binary files differnew file mode 100644 index 0000000..164d3eb --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/tree-dark-linux.png diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/tree-light-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/tree-light-linux.png Binary files differnew file mode 100644 index 0000000..ed51114 --- /dev/null +++ b/tools/copland/tests/visual.spec.mjs-snapshots/tree-light-linux.png |
