initial public commit

This commit is contained in:
2026-02-22 21:26:15 -05:00
commit 9dbf7ae796
100 changed files with 18823 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
document.addEventListener("DOMContentLoaded", () => {
// add default #overview route
if (!window.location.hash) {
window.location.hash = "overview";
}
});

View File

@@ -0,0 +1,12 @@
document.addEventListener("DOMContentLoaded", () => {
const currentHost = window.location.hostname;
const links = document.querySelectorAll("a");
links.forEach((link) => {
if (link.hostname && link.hostname !== currentHost) {
link.setAttribute("target", "_blank");
link.setAttribute("rel", "noopener noreferrer");
}
});
});

View File

@@ -0,0 +1,44 @@
export default function formatSource(source, panelId) {
source = (source ?? "").trim();
if (!source || !panelId) return;
const sourceSplit = source.split(/\r?\n/);
const spaceCount = sourceSplit[sourceSplit.length - 1].search(/\S/);
if (spaceCount <= 0) return; // empty string
const trimRE = new RegExp(`^\\s{${spaceCount}}`);
const preDom = document.createElement("pre");
preDom.append(sourceSplit.map((val) => val.replace(trimRE, "")).join("\n"));
preDom.innerHTML = preDom.innerHTML
// strRegEx must be applied first to prevent false positives
.replace(/\"[^"]+\"/g, (val) =>
val !== '"module"' ? `<span class="code-str">${val}</span>` : val,
)
.replace(/\/\/.*/g, (val) => `<span class="code-cmnt">${val}</span>`)
.replace(/\`[^`]+\`/g, (val) => `<span class="code-str">${val}</span>`)
.replace(/\.\w+/g, (val) =>
val !== ".js" ? `<span class="code-func">${val}</span>` : val,
)
.replace(
/&lt;\/?script[^&]*&gt;/g,
(val) => `<span class="code-cmd">${val}</span>`,
);
const sourceHTML = `
<details class='js-sourceDetails' open="true">
<summary>Source</summary>
<div>
<figure>
${preDom.outerHTML}
</figure>
</div>
</details>
`.trim();
document
.getElementById(panelId)
.insertAdjacentHTML("beforeend", sourceHTML);
}

View File

@@ -0,0 +1,63 @@
import { hashTwt } from "/dist-browser/twtxt-lib.js";
let wasHashTwtResultAppended = false;
const formHash = document.forms["formHash"];
formHash.addEventListener("submit", (e) => {
e.preventDefault();
const content = formHash.elements["content"].value;
const created = formHash.elements["created"].value;
const url = formHash.elements["url"].value;
const hash = hashTwt({
content,
created,
url,
});
const result = [
`content: ${content}`,
`created: ${created}`,
`url: ${url}`,
`hash: ${hash}`,
].join("\n");
console.log((wasHashTwtResultAppended ? "\n" : "") + result);
const resultHTML = result
.split("\n")
.map((line) =>
line.replace(
// to color properties
/^\w+:/,
(val) => `<span class="code-str">${val}</span>`,
),
)
.join("\n");
if (wasHashTwtResultAppended) {
document
.getElementById("preHashTwtResult")
.insertAdjacentHTML("afterbegin", resultHTML + "<br />\n");
return;
}
const resultsHTML = `
<details open="">
<summary>Results</summary>
<figure>
<pre id="preHashTwtResult">${resultHTML}</pre>
</figure>
</details>
`.trim();
document
.getElementById("tabHashTwt-panel")
.insertAdjacentHTML("beforeend", resultsHTML);
document
.querySelector("#tabHashTwt-panel .js-sourceDetails")
?.removeAttribute("open");
document.body.classList.add("isLoaded");
wasHashTwtResultAppended = true;
});

View File

@@ -0,0 +1,40 @@
import formatSource from "./format-source.js";
formatSource(
`
\<script type="module">
import { hashTwt } from "/web/dist/twtxt-lib.js";
let wasHashTwtResultAppended = false;
const formHash = document.forms["formHash"];
formHash.addEventListener("submit", (e) => {
e.preventDefault();
const content = formHash.elements["content"].value;
const created = formHash.elements["created"].value;
const url = formHash.elements["url"].value;
const hash = hashTwt({
content,
created,
url,
});
const result = [
\`content: \${content}\`,
\`created: \${created}\`,
\`url: \${url}\`,
\`hash: \${hash}\`,
].join("\\n");
console.log(
(wasHashTwtResultAppended ? "\\n" : "") + result,
);
wasHashTwtResultAppended = true;
});
<\/script>
`,
"tabHashTwt-panel",
);

View File

@@ -0,0 +1,79 @@
import { loadAndParseTwtxtFile } from "/dist-browser/twtxt-lib.js";
const tabLoadAndParsePanel = document.getElementById("tabLoadAndParse-panel");
let wasLoadAndParseResultAppended = false;
document
.getElementById("formLoadAndParse")
.addEventListener("submit", async (ev) => {
ev?.preventDefault();
tabLoadAndParsePanel.classList.add("isLoading");
const loadAndParseURL = document.getElementById("loadAndParseURL");
const url =
loadAndParseURL?.value || "/twtxt-demos/demo-hipster-twtxt.txt";
const parsedFile = await loadAndParseTwtxtFile(url);
console.log(parsedFile);
tabLoadAndParsePanel.classList.remove("isLoading");
if (wasLoadAndParseResultAppended) {
document.getElementById("preLoadAndParseResult").outerHTML = `
<pre id="preLoadAndParseResult">${JSON.stringify(
parsedFile,
null,
2,
).replace(
// to color properties
/"\w+":/g,
(val) => `<span class="code-str">${val}</span>`,
)}</pre>
`;
return;
}
const resultsHTML = `
<details open="true">
<summary>Results</summary>
<figure>
<pre id="preLoadAndParseResult">${JSON.stringify(
parsedFile,
null,
2,
).replace(
// to color properties
/"\w+":/g,
(val) => `<span class="code-str">${val}</span>`,
)}</pre>
</figure>
</details>
`;
tabLoadAndParsePanel.insertAdjacentHTML("beforeend", resultsHTML);
document
.querySelector("#tabLoadAndParse-panel .js-sourceDetails")
?.removeAttribute("open");
document.body.classList.add("isLoaded");
wasLoadAndParseResultAppended = true;
});
const loadAndParseClickHandler = (ev) => {
ev?.preventDefault();
loadAndParseURL.value = ev.target.dataset.url;
};
[
"loadAndParseHipsterButton",
"loadAndParsePirateButton",
"loadAndParseSaganButton",
].forEach((curr) => {
document
.getElementById(curr)
.addEventListener("click", loadAndParseClickHandler);
});

View File

@@ -0,0 +1,25 @@
import formatSource from "./format-source.js";
formatSource(
`
\<script type="module">
import { loadAndParseTwtxtFile } from "/web/dist/twtxt-lib.js";
document
.getElementById("formLoadAndParse")
.addEventListener("submit", async (ev) => {
ev?.preventDefault();
const url =
document.getElementById("loadAndParseURL")?.value ??
"/twtxt-demos/demo-hipster-twtxt.txt";
const parsedFile = await loadAndParseTwtxtFile(url);
console.log(parsedFile);
});
<\/script>
`,
"tabLoadAndParse-panel",
);

View File

@@ -0,0 +1,36 @@
import { loadAndParseTwtxtFile } from "/dist-browser/twtxt-lib.js";
// run in an IIFE (or event listener) to avoid issues with top-level await
(async () => {
try {
const parsedFile = await loadAndParseTwtxtFile(
"/twtxt-demos/demo-hipster-twtxt.txt",
);
console.log(parsedFile);
document.getElementById("tabOverview-example")?.insertAdjacentHTML(
"beforeend",
`
<details open="true">
<summary>Result</summary>
<figure>
<pre id="preResult">${JSON.stringify(
parsedFile,
null,
2,
).replace(
// to color properties
/"\w+":/g,
(val) => `<span class="code-str">${val}</span>`,
)}</pre>
</figure>
</details>
`,
);
document.body.classList.add("isLoaded");
} catch (err) {
console.error(err);
}
})();

View File

@@ -0,0 +1,24 @@
import formatSource from "./format-source.js";
formatSource(
`
\<script type="module">
import { loadAndParseTwtxtFile } from "/web/dist/twtxt-lib.js";
// run in an IIFE (or event listener) to avoid issues with top-level await
(async () => {
try {
const parsedFile = await loadAndParseTwtxtFile(
"/twtxt-demos/demo-hipster-twtxt.txt",
);
console.log(parsedFile);
} catch (err) {
console.error(err);
}
})();
<\/script>
`,
"tabOverview-example",
);

View File

@@ -0,0 +1,76 @@
import { parseTwtxt } from "/dist-browser/twtxt-lib.js";
const tabParsePanel = document.getElementById("tabParse-panel");
let wasParseResultAppended = false;
document.getElementById("formParse").addEventListener("submit", async (ev) => {
ev?.preventDefault();
tabParsePanel.classList.add("isLoading");
const parseURL = document.getElementById("parseURL");
const url = parseURL?.value ?? "/twtxt-demos/demo-hipster-twtxt.txt";
const response = await fetch(url);
const twtxtFile = await response.text();
const parsedFile = parseTwtxt(twtxtFile);
console.log(parsedFile);
tabParsePanel.classList.remove("isLoading");
if (wasParseResultAppended) {
document.getElementById("preParseResult").outerHTML = `
<pre id="preParseResult">${JSON.stringify(
parsedFile,
null,
2,
).replace(
// to color properties
/"\w+":/g,
(val) => `<span class="code-str">${val}</span>`,
)}</pre>
`;
return;
}
const resultsHTML = `
<details open="true">
<summary>Results</summary>
<figure>
<pre id="preParseResult">${JSON.stringify(
parsedFile,
null,
2,
).replace(
// to color properties
/"\w+":/g,
(val) => `<span class="code-str">${val}</span>`,
)}</pre>
</figure>
</details>
`;
tabParsePanel.insertAdjacentHTML("beforeend", resultsHTML);
document
.querySelector("#tabParse-panel .js-sourceDetails")
?.removeAttribute("open");
document.body.classList.add("isLoaded");
wasParseResultAppended = true;
});
const parseClickHandler = (ev) => {
ev?.preventDefault();
parseURL.value = ev.target.dataset.url;
};
["parseHipsterButton", "parsePirateButton", "parseSaganButton"].forEach(
(curr) => {
document
.getElementById(curr)
.addEventListener("click", parseClickHandler);
},
);

View File

@@ -0,0 +1,27 @@
import formatSource from "./format-source.js";
formatSource(
`
\<script type="module">
import { parseTwtxt } from "/web/dist/twtxt-lib.js";
document
.getElementById("formParse")
.addEventListener("submit", async (ev) => {
ev?.preventDefault();
const url =
document.getElementById("parseURL")?.value ??
"/twtxt-demos/demo-hipster-twtxt.txt";
const response = await fetch(url);
const twtxtFile = await response.text();
const parsedFile = parseTwtxt(twtxtFile);
console.log(parsedFile);
});
<\/script>
`,
"tabParse-panel",
);

471
dist-demo/demo/styles.css Normal file
View File

@@ -0,0 +1,471 @@
:root {
--fg-main: #DBDFAC;
--bg-main: #3B1F2B;
--fg-light: #edefd5;
--bg-dark: rgba(10, 10, 10, .5);
--bg-light: rgba(245, 245, 245, .6);
--main-link: #8598AD;
--link-active: #ccc;
--link-active: lch(from var(--main-link) calc(l + 20) c h);
--gray-light: #5F758E;
}
@keyframes riseInDetails {
0% {opacity: 0; margin-top: 2rem}
100% {opacity: 1; margin-top: 0rem}
}
@keyframes riseInTab {
0% {opacity: 0; margin-top: -2rem}
100% {opacity: 1; margin-top: -4rem}
}
* {
box-sizing: border-box;
scrollbar-width: thin;
}
html {
font-size: 18px;
}
body {
background-color: var(--bg-main);
color: var(--fg-main);
margin: 0;
transition:
background-color .5s,
border-color .5s,
color .5s;
}
a {
border: 1px solid transparent;
border-bottom-color: var(--main-link);
border-radius: 0;
color: var(--main-link);
padding: 0 .5rem ;
text-decoration: none;
transition: all .5s;
}
a:active {
background-color: var(--fg-main);
border-color: var(--bg-main);
color: var(--link-active);
}
a:hover {
border-color: var(--link-active);
border-radius: .5rem;
color: var(--link-active);
}
button {
background-color: var(--link-active);
}
label {
display: inline-block;
border: none;
max-width: 25rem;
padding: 1rem;
text-align: center;
width: 100%;
}
input {
background-color: var(--fg-main);
}
input[type="reset"], input[type="submit"] {
background-color: var(--link-active);
}
input[type="text"], input[type="url"] {
background-color: var(--fg-light);
font-size: 1rem;
max-width: 25rem;
width: 100%;
}
pre {
background-color: var(--bg-dark);
border: 1px solid var(--fg-main);
color: var(--fg-main);
font-size: smaller;
padding: 1rem;
white-space: pre-wrap;
}
textarea {
background-color: var(--fg-light);
font-size: 1rem;
max-width: 30rem;
width: 100%;
}
details figure {
margin: 1rem .5rem;
}
details figure pre {
overflow: auto;
white-space: pre;
}
details[open] summary ~ * {
animation: riseInDetails .5s ease-in-out;
}
summary {
cursor: pointer;
}
.code-cmd {
color: var(--gray-light);
}
.code-cmnt {
color: #ccc;
}
.code-str {
color: var(--gray-light);
}
.copyright {
font-style: italic;
}
.flexCol {
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
max-width: 25rem;
width: 100%;
}
.flexRow {
align-items: center;
display: flex;
flex-direction: column;
justify-content: space-around;
padding: .5rem 1rem;
}
.tab {
border: 2px solid transparent;
border-top: 2px solid var(--fg-main);
border-radius: .5rem .5rem 0 0;
display: block;
}
.tab-link {
background-color: var(--bg-main);
border-radius: .5rem .5rem 0 0;
border-bottom: 0;
color: var(--main-link);
display: block;
padding: 1rem 2rem;
position: relative;
text-decoration: none;
}
.tab-panel {
bottom: 0;
display: none;
left: 0;
overflow: auto;
padding: 0 1rem 1rem;
right: 0;
top: 4.25rem;
width: 100%;
z-index: -2;
}
.tabs {
border: 2px solid var(--fg-main);
border-top-color: transparent;
border-radius: .5rem .5rem 0 0;
display: flex;
flex-direction: column;
list-style: none;
margin: 0;
min-height: 100vh;
padding: 0;
}
.tab:target .tab-panel {
display: block;
}
body:not(:has(:target)) #tabOverview-panel {
display: block;
}
.tab:target .tab-link {
background: linear-gradient(var(--bg-dark), var(--bg-main));
}
.tab:target + .tab {
margin-top: auto;
}
body:not(:has(:target)) #tabOverview-link {
background-color: var(--bg-dark);
color: var(--fg-main);
}
.themeToggle-button {
background-color: var(--fg-main);
border: 1px solid var(--gray-light);
border-radius: 1rem;
bottom: .5rem;
color: var(--gray-light);
display: flex;
font-size: 1.5rem;
padding: .5rem;
position: fixed;
right: .5rem;
z-index: 100;
}
.themeToggle-svgIndicator {
rotate: 180deg;
transition: rotate .5s;
}
/** Loader from https://www.cssportal.com/css-loader-generator/ */
.dotLoader {
animation: dotLoaderFrames 1s infinite steps(6);
background:
linear-gradient(var(--bg-main) 0 0) left -25% top 0 /20% 100% no-repeat var(--fg-main);
display: block;
height: 20px;
margin-top: 1rem;
mask: linear-gradient(90deg,var(--bg-main) 70%,#0000 0) left/20% 100%;
-webkit-mask: linear-gradient(90deg,var(--bg-main) 70%,#0000 0) left/20% 100%;
transition: opacity .5s;
width: 120px;
}
@keyframes dotLoaderFrames {
100% {background-position: right -25% top 0}
}
/** ID Overrides */
#formHash .flexCol {
min-width: 15rem;
width: 50%;
}
#formHash input[type="submit"] {
margin-top: 1rem;
min-width: 5rem;
}
#formHash textarea {
min-height: 6rem;
width: 100%
}
#formLoadAndParse .flexCol {
width: 100%;
max-width: 25rem;
}
#formLoadAndParse label {
width: 100%;
max-width: 25rem;
}
/** Media-Query Overrides */
@media (min-width: 900px) {
.flexRow {
flex-direction: row;
}
.tab {
border-color: transparent;
display: inline-block;
position: static;
}
.tab-link {
background-color: transparent;;
border: 2px solid var(--fg-main);
border-bottom-color: transparent;
margin-top: -4.5rem;
}
.tab-link:hover {
background-color: var(--link-active);
border-radius: .5rem .5rem 0 0;
color: var(--bg-main);
font-size: 1.2rem;
margin-top: -4.65rem;
}
.tab-panel {
animation: riseInTab .5s ease-in-out;
margin-top: -4rem;
position: relative;
padding: 0 2rem;
}
.tabs {
border-top: 2px solid var(--fg-main);
bottom: 0;
flex-direction: row;
left: 0;
margin-top: 4.5rem;
min-height: auto;
padding: 1rem;
position: absolute;
right: 0;
top: 0;
}
.tab:target + .tab {
margin-top: 0;
}
.tab:target .tab-link {
border-bottom: 0;
font-size: 1.2rem;
margin-top: -4.55rem;
}
.tab:target .tab-link:hover {
border-radius: .5rem .5rem 0 0;
background-color: var(--bg-dark);
color: var(--main-link);
font-size: 1.2rem;
margin-top: -4.55rem;
}
.tab:target .tab-panel {
position: absolute;
z-index: 1;
}
body:not(:has(:target)) #tabOverview-panel {
display: block;
position: absolute;
z-index: 1;
}
.themeToggle-button {
bottom: auto;
position: absolute;
right: 1rem;
top: 1rem;
}
}
/** State-Based Overrides */
body.invertedTheme {
background-color: var(--fg-main);
color: var(--bg-main);
}
body.invertedTheme a:hover {
background-color: var(--main-link);
border-color: var(--bg-dark);
color: var(--fg-main);
}
body.invertedTheme figcaption {
border-color: var(--bg-main);
}
body.invertedTheme figure {
background-color: var(--gray-light);
border-color: var(--bg-main);
}
body.invertedTheme input,
body.invertedTheme textarea {
background-color: var(--fg-light);
}
body.invertedTheme pre {
background-color: var(--bg-light);
border-color: var(--bg-main);
color: var(--bg-main);
}
body.invertedTheme .code-cmd {
color: var(--bg-dark);
}
body.invertedTheme .code-str {
color: var(--bg-dark);
}
body.invertedTheme .tab-link {
color: var(--bg-dark);
background-color: var(--fg-main);
border-color: var(--bg-main);
}
body.invertedTheme .tab:target .tab-link {
background: linear-gradient(var(--link-active), var(--fg-main));
}
body.invertedTheme .tab-link:hover {
background-color: var(--main-link);
border-color: var(--bg-dark);
}
body.invertedTheme .tab:target .tab-link {
border-color: var(--bg-main);
}
body.invertedTheme .tab-panel {
border-color: var(--bg-main);
}
body.invertedTheme .tabs {
border: 2px solid var(--bg-main);
}
body.invertedTheme .themeToggle-button {
background-color: var(--bg-main);
color: var(--main-link);
}
body.invertedTheme .themeToggle-svgIndicator {
rotate: 0deg;
}
.isLoaded .dotLoader {
margin: 0;
max-height: 0;
opacity: 0;
}
.isLoading .dotLoader {
opacity: 1;
}
a[href^='http']::after {
content: '\2197'; /* Code for ↗ */
display: inline-block;
margin-left: 5px;
font-size: 0.9em;
}

View File

@@ -0,0 +1,29 @@
document.addEventListener("DOMContentLoaded", () => {
const toggle = document.createElement("button");
toggle.classList.add("themeToggle-button");
toggle.setAttribute("id", "themeToggle-button");
toggle.addEventListener("click", () => {
document.body.classList.toggle("invertedTheme");
});
toggle.innerHTML = `
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
width="1em"
height="1em"
class="themeToggle-svgIndicator"
fill="currentColor"
viewBox="0 0 32 32"
>
<path
d="M16 .5C7.4.5.5 7.4.5 16S7.4 31.5 16 31.5 31.5 24.6 31.5 16 24.6.5
16 .5zm0 28.1V3.4C23 3.4 28.6 9 28.6 16S23 28.6 16 28.6z"
/>
</svg>
`.trim();
document.body.appendChild(toggle);
});