add support for v2 hashing algorithm.
update README.md and demo file to be more in sync. update to v0.10.0.
This commit is contained in:
187
README.md
187
README.md
@@ -12,14 +12,30 @@ These functions include:
|
|||||||
[metadata](https://twtxt.dev/exts/metadata.html) in the file).
|
[metadata](https://twtxt.dev/exts/metadata.html) in the file).
|
||||||
- `loadAndParseTwtxt`: fetches a twtxt file from _the internet_ and parses it into an object (as above).
|
- `loadAndParseTwtxt`: fetches a twtxt file from _the internet_ and parses it into an object (as above).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Isomorphic, available as an ([optionally minified](/dist-browser/twtxt-lib.min.js))
|
||||||
|
[ES6+ library for the browser](/dist-browser/twtxt-lib.js), with NPM and JSR versions coming soon.
|
||||||
|
or as a package (from [NPM](https://www.npmjs.com/package/twtxt-lib) or
|
||||||
|
[JSR](https://jsr.io/@itsericwoodward/twtxt-lib)).
|
||||||
|
- Fully typed and source-mapped (I hope).
|
||||||
|
- Built as an [ES6 Module](https://caniuse.com/es6-module) (and [ESM only](https://antfu.me/posts/move-on-to-esm-only)).
|
||||||
|
- Includes sample files and an [interactive demo](https://twtxt-lib.itsericwoodward.com/).
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
This library can be installed in several different ways:
|
This library can be installed several different ways:
|
||||||
|
|
||||||
### For the Browser
|
### For the Browser
|
||||||
|
|
||||||
1. Grab the latest copy of the `twtxt-lib.js` file, either by downloading it from the [git repo](https://git.itsericwoodward.com/eric/twtxt-lib/raw/branch/main/dist-browser/twtxt-lib.js), the [website](https://twtxt-lib.itsericwoodward.com/dist-browser/twtxt-lib.js), or doing a `git clone https://git.itsericwoodward.com/eric/twtxt-lib.git` and pulling it out of the `dist-browser` folder.
|
1. Grab the latest copy of the `twtxt-lib.js` file, either by downloading it from the
|
||||||
- Alternatively, you can grab the minified version from the [same](https://git.itsericwoodward.com/eric/twtxt-lib/raw/branch/main/dist-browser/twtxt-lib.min.js) [sources](https://twtxt-lib.itsericwoodward.com/dist-browser/twtxt-lib.min.js).
|
[git repo](https://git.itsericwoodward.com/eric/twtxt-lib/raw/branch/main/dist-browser/twtxt-lib.js),
|
||||||
|
the [website](https://twtxt-lib.itsericwoodward.com/dist-browser/twtxt-lib.js), or by doing a
|
||||||
|
`git clone https://git.itsericwoodward.com/eric/twtxt-lib.git` and pulling it out of the
|
||||||
|
`dist-browser` folder.
|
||||||
|
- Alternatively, you can grab the minified version from the
|
||||||
|
[same](https://git.itsericwoodward.com/eric/twtxt-lib/raw/branch/main/dist-browser/twtxt-lib.min.js)
|
||||||
|
[sources](https://twtxt-lib.itsericwoodward.com/dist-browser/twtxt-lib.min.js).
|
||||||
2. Add the newly acquired file to your static site / progressive web app / over-engineered blog.
|
2. Add the newly acquired file to your static site / progressive web app / over-engineered blog.
|
||||||
3. Import the desired function(s) via ESM: `import { hashTwt, loadAndParseTwtxtFile } from "./twtxt-lib.js";`
|
3. Import the desired function(s) via ESM: `import { hashTwt, loadAndParseTwtxtFile } from "./twtxt-lib.js";`
|
||||||
|
|
||||||
@@ -27,88 +43,28 @@ This library can be installed in several different ways:
|
|||||||
|
|
||||||
1. Add the package to your project.
|
1. Add the package to your project.
|
||||||
- Via [NPM](https://www.npmjs.com/package/twtxt-lib): `yarn add twtxt-lib`
|
- Via [NPM](https://www.npmjs.com/package/twtxt-lib): `yarn add twtxt-lib`
|
||||||
- Via [JSR](https://jsr.io/@itsericwoodward/twtxt-lib): `yarn add jsr:@itsericwoodward/twtxt-lib`
|
- Via [JSR](https://jsr.io/@itsericwoodward/twtxt-lib):
|
||||||
2. Import the desired function(s) into your code: `import { hashTwt, loadAndParseTwtxtFile } from "twtxt-lib";`
|
`yarn add jsr:@itsericwoodward/twtxt-lib`
|
||||||
|
2. Import the desired function(s) via ESM:
|
||||||
## Features
|
`import { hashTwt, loadAndParseTwtxtFile, parseTwtxt } from "twtxt-lib";`
|
||||||
|
|
||||||
- Isomorphic, available as an ([optionally minified](/dist-browser/twtxt-lib.min.js))
|
|
||||||
[ES6+ library for the browser](/dist-browser/twtxt-lib.js), with NPM and JSR versions coming soon.
|
|
||||||
- Fully typed and source-mapped.
|
|
||||||
- [ES6 Module](https://caniuse.com/es6-module) (and [ESM only](https://antfu.me/posts/move-on-to-esm-only)).
|
|
||||||
- Includes sample files and an [interactive demo page](https://twtxt-lib.itsericwoodward.com/).
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### loadAndParseText
|
See the included tests and demo file for more information on how to use it.
|
||||||
|
|
||||||
This is arguably the most useful function:
|
|
||||||
|
|
||||||
```
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
```
|
|
||||||
|
|
||||||
Result:
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"following": [
|
|
||||||
{
|
|
||||||
"nick": "demo_hipster",
|
|
||||||
"url": "https://example.com/demo-hipster-twtxt.txt"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"nick": "demo_pirate",
|
|
||||||
"url": "https://example.org/~pirate/twtxt.txt"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"nick": "demo_sagan",
|
|
||||||
"url": "https://example.net/~saganos/twtxt.txt"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"metadata": {
|
|
||||||
"nick": "demo_hipster",
|
|
||||||
"url": "https://example.com/demo-hipster-twtxt.txt",
|
|
||||||
"avatar": "https://i.pravatar.cc/150?img=67",
|
|
||||||
"description": "Kitsch ut post-ironic, bruh tilde non shabby chic iceland fixie consequat?"
|
|
||||||
},
|
|
||||||
"twts": [
|
|
||||||
{
|
|
||||||
"content": "Normcore tilde ad selfies, culpa cupping nostrud gatekeep aesthetic PBR&B 3 wolf moon mustache twee.",
|
|
||||||
"created": "2025-06-02T18:47:01+01:00",
|
|
||||||
"createdUTC": "2025-06-02T17:47:01.000Z",
|
|
||||||
"hash": "ymiydvq"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"content": "Cardigan jean shorts eu 90's. Kitsch knausgaard culpa, marfa mumblecore portland raclette banjo retro exercitation pariatur snackwave williamsburg.",
|
|
||||||
"created": "2025-07-05T00:17:46+02:00",
|
|
||||||
"createdUTC": "2025-07-04T22:17:46.000Z",
|
|
||||||
"hash": "c6bm4sq"
|
|
||||||
},
|
|
||||||
...
|
|
||||||
],
|
|
||||||
"lastModified": "2026-02-22T20:56:59.000Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
When using in a web browser, be aware of potential the potential for issues with [Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS).
|
|
||||||
|
|
||||||
### hashTwt
|
### hashTwt
|
||||||
|
|
||||||
|
A function that takes the constituent parts of a "twt" and generates an
|
||||||
|
[extension-compatible hash](https://twtxt.dev/exts/twt-hash.html) for it, which is then returned.
|
||||||
|
|
||||||
|
Version 0.10.0 and above includes support for
|
||||||
|
[V2 of the Hashing Spec](https://git.mills.io/yarnsocial/twtxt.dev/pulls/28):
|
||||||
|
|
||||||
|
- A specific hashing version can be provided as an optional argument.
|
||||||
|
- When no version argument is provided, it defaults to using version 1 for all twts with a created
|
||||||
|
date before the epoch date (`2026-07-01T00:00:00Z`), and version 2 for all twts created on or
|
||||||
|
after the epoch.
|
||||||
|
|
||||||
```
|
```
|
||||||
import { hashTwt } from "/web/dist/twtxt-lib.js";
|
import { hashTwt } from "/web/dist/twtxt-lib.js";
|
||||||
|
|
||||||
@@ -129,6 +85,10 @@ Hash: 7uftieq
|
|||||||
|
|
||||||
### parseTwtxt
|
### parseTwtxt
|
||||||
|
|
||||||
|
A function that parses a twtxt file string, returning an object with information about the file and
|
||||||
|
its owner (including generating [hashes](#hashTwt) for each twt and
|
||||||
|
any [metadata](https://twtxt.dev/exts/metadata.html) in the file).
|
||||||
|
|
||||||
```
|
```
|
||||||
import { base32Encode, hashTwt, loadAndParseTwtxt, parseTwtxt } from "twtxt-lib";
|
import { base32Encode, hashTwt, loadAndParseTwtxt, parseTwtxt } from "twtxt-lib";
|
||||||
|
|
||||||
@@ -213,7 +173,74 @@ Result:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
See the included tests and demo file for more information on how to use it.
|
### loadAndParseText
|
||||||
|
|
||||||
|
Aan async function that fetches a `twtxt.txt`-compatible file from a URL and parses it,
|
||||||
|
returning the extracted data as an object.
|
||||||
|
|
||||||
|
```
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
Result:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"following": [
|
||||||
|
{
|
||||||
|
"nick": "demo_hipster",
|
||||||
|
"url": "https://example.com/demo-hipster-twtxt.txt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nick": "demo_pirate",
|
||||||
|
"url": "https://example.org/~pirate/twtxt.txt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nick": "demo_sagan",
|
||||||
|
"url": "https://example.net/~saganos/twtxt.txt"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"nick": "demo_hipster",
|
||||||
|
"url": "https://example.com/demo-hipster-twtxt.txt",
|
||||||
|
"avatar": "https://i.pravatar.cc/150?img=67",
|
||||||
|
"description": "Kitsch ut post-ironic, bruh tilde non shabby chic iceland fixie consequat?"
|
||||||
|
},
|
||||||
|
"twts": [
|
||||||
|
{
|
||||||
|
"content": "Normcore tilde ad selfies, culpa cupping nostrud gatekeep aesthetic PBR&B 3 wolf moon mustache twee.",
|
||||||
|
"created": "2025-06-02T18:47:01+01:00",
|
||||||
|
"createdUTC": "2025-06-02T17:47:01.000Z",
|
||||||
|
"hash": "ymiydvq"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "Cardigan jean shorts eu 90's. Kitsch knausgaard culpa, marfa mumblecore portland raclette banjo retro exercitation pariatur snackwave williamsburg.",
|
||||||
|
"created": "2025-07-05T00:17:46+02:00",
|
||||||
|
"createdUTC": "2025-07-04T22:17:46.000Z",
|
||||||
|
"hash": "c6bm4sq"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
],
|
||||||
|
"lastModified": "2026-02-22T20:56:59.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When using in a web browser, be aware of the potential for issues with
|
||||||
|
[Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
2
dist-browser/constants.d.ts
vendored
2
dist-browser/constants.d.ts
vendored
@@ -1 +1 @@
|
|||||||
export declare const __dirname: string;
|
export declare const HASH_V2_EPOCH = "2026-07-01T00:00:00Z";
|
||||||
|
|||||||
3
dist-browser/hashTwt.d.ts
vendored
3
dist-browser/hashTwt.d.ts
vendored
@@ -1,2 +1,3 @@
|
|||||||
import { Twt } from './types.ts';
|
import { Twt } from './types.ts';
|
||||||
export default function hashTwt(twt: Twt): string;
|
export type HashableTwt = Pick<Twt, "content" | "created" | "url"> & Partial<Omit<Twt, "content" | "created" | "url">>;
|
||||||
|
export default function hashTwt(twt: HashableTwt, version?: number): string;
|
||||||
|
|||||||
@@ -2900,6 +2900,7 @@ const base32Encode = (payload) => {
|
|||||||
return encoder.write(payload).finalize();
|
return encoder.write(payload).finalize();
|
||||||
};
|
};
|
||||||
const getValueOrFirstEntry = (value) => Array.isArray(value) && value.length ? value[0] : value;
|
const getValueOrFirstEntry = (value) => Array.isArray(value) && value.length ? value[0] : value;
|
||||||
|
const HASH_V2_EPOCH = `2026-07-01T00:00:00Z`;
|
||||||
const dateRegex = /^(\d{4})-(\d{2})-(\d{2})([tT ])(\d{2}):(\d{2}):(\d{2})\.?(\d{3})?(?:(?:([+-]\d{2}):?(\d{2}))|Z)?$/;
|
const dateRegex = /^(\d{4})-(\d{2})-(\d{2})([tT ])(\d{2}):(\d{2}):(\d{2})\.?(\d{3})?(?:(?:([+-]\d{2}):?(\d{2}))|Z)?$/;
|
||||||
const formatRFC3339 = (date) => {
|
const formatRFC3339 = (date) => {
|
||||||
const pad = (num = 0) => `${+num < 10 ? 0 : ""}${+num}`;
|
const pad = (num = 0) => `${+num < 10 ? 0 : ""}${+num}`;
|
||||||
@@ -2928,10 +2929,14 @@ const formatRFC3339 = (date) => {
|
|||||||
offset
|
offset
|
||||||
].join("");
|
].join("");
|
||||||
};
|
};
|
||||||
function hashTwt(twt) {
|
function hashTwt(twt, version = 0) {
|
||||||
const created = formatRFC3339(twt.created);
|
const created = formatRFC3339(twt.created);
|
||||||
const payload = [twt.url, created, twt.content].join("\n");
|
const payload = [twt.url, created, twt.content].join("\n");
|
||||||
return base32Encode(blakejsExports.blake2b(payload, void 0, 32)).toLowerCase().slice(-7);
|
const encoded = base32Encode(blakejsExports.blake2b(payload, void 0, 32)).toLowerCase();
|
||||||
|
const createdDate = new Date(twt.created), epochDate = new Date(HASH_V2_EPOCH);
|
||||||
|
if (version === 1 || !version && createdDate < epochDate)
|
||||||
|
return encoded.slice(-7);
|
||||||
|
return encoded.slice(0, 12);
|
||||||
}
|
}
|
||||||
var dayjs_min$1 = { exports: {} };
|
var dayjs_min$1 = { exports: {} };
|
||||||
var dayjs_min = dayjs_min$1.exports;
|
var dayjs_min = dayjs_min$1.exports;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
800
dist-browser/twtxt-lib.min.js
vendored
800
dist-browser/twtxt-lib.min.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -9,17 +9,22 @@ formHash.addEventListener("submit", (e) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const content = formHash.elements["content"].value;
|
const content = formHash.elements["content"].value;
|
||||||
const created = formHash.elements["created"].value;
|
const created = formHash.elements["created"].value;
|
||||||
|
const version = parseInt(formHash.elements["version"].value, 10);
|
||||||
const url = formHash.elements["url"].value;
|
const url = formHash.elements["url"].value;
|
||||||
const hash = hashTwt({
|
const hash = hashTwt(
|
||||||
content,
|
{
|
||||||
created,
|
content,
|
||||||
url,
|
created,
|
||||||
});
|
url,
|
||||||
|
},
|
||||||
|
version,
|
||||||
|
);
|
||||||
|
|
||||||
const result = [
|
const result = [
|
||||||
`content: ${content}`,
|
`content: ${content}`,
|
||||||
`created: ${created}`,
|
`created: ${created}`,
|
||||||
`url: ${url}`,
|
`url: ${url}`,
|
||||||
|
`version: ${version}`,
|
||||||
`hash: ${hash}`,
|
`hash: ${hash}`,
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
||||||
|
|||||||
@@ -15,18 +15,23 @@ formatSource(
|
|||||||
|
|
||||||
const content = formHash.elements["content"].value;
|
const content = formHash.elements["content"].value;
|
||||||
const created = formHash.elements["created"].value;
|
const created = formHash.elements["created"].value;
|
||||||
|
const version = parseInt(formHash.elements["version"].value, 10);
|
||||||
const url = formHash.elements["url"].value;
|
const url = formHash.elements["url"].value;
|
||||||
|
|
||||||
const hash = hashTwt({
|
const hash = hashTwt(
|
||||||
content,
|
{
|
||||||
created,
|
content,
|
||||||
url,
|
created,
|
||||||
});
|
url,
|
||||||
|
},
|
||||||
|
version,
|
||||||
|
);
|
||||||
|
|
||||||
const result = [
|
const result = [
|
||||||
\`content: \${content}\`,
|
\`content: \${content}\`,
|
||||||
\`created: \${created}\`,
|
\`created: \${created}\`,
|
||||||
\`url: \${url}\`,
|
\`url: \${url}\`,
|
||||||
|
\`version: \${version}\`,
|
||||||
\`hash: \${hash}\`,
|
\`hash: \${hash}\`,
|
||||||
].join("\\n");
|
].join("\\n");
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,18 @@ code {
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
details figure {
|
||||||
|
margin: 1rem .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
details figure pre {
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
details[open] summary ~ * {
|
||||||
|
animation: riseInDetails .5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -110,6 +122,15 @@ pre {
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
background-color: var(--fg-light);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
select:open {
|
||||||
|
background-color: var(--link-active);
|
||||||
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
background-color: var(--fg-light);
|
background-color: var(--fg-light);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
@@ -117,21 +138,6 @@ textarea {
|
|||||||
width: 100%;
|
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 {
|
summary {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|||||||
2
dist-demo/dist-browser/constants.d.ts
vendored
2
dist-demo/dist-browser/constants.d.ts
vendored
@@ -1 +1 @@
|
|||||||
export declare const __dirname: string;
|
export declare const HASH_V2_EPOCH = "2026-07-01T00:00:00Z";
|
||||||
|
|||||||
3
dist-demo/dist-browser/hashTwt.d.ts
vendored
3
dist-demo/dist-browser/hashTwt.d.ts
vendored
@@ -1,2 +1,3 @@
|
|||||||
import { Twt } from './types.ts';
|
import { Twt } from './types.ts';
|
||||||
export default function hashTwt(twt: Twt): string;
|
export type HashableTwt = Pick<Twt, "content" | "created" | "url"> & Partial<Omit<Twt, "content" | "created" | "url">>;
|
||||||
|
export default function hashTwt(twt: HashableTwt, version?: number): string;
|
||||||
|
|||||||
@@ -2900,6 +2900,7 @@ const base32Encode = (payload) => {
|
|||||||
return encoder.write(payload).finalize();
|
return encoder.write(payload).finalize();
|
||||||
};
|
};
|
||||||
const getValueOrFirstEntry = (value) => Array.isArray(value) && value.length ? value[0] : value;
|
const getValueOrFirstEntry = (value) => Array.isArray(value) && value.length ? value[0] : value;
|
||||||
|
const HASH_V2_EPOCH = `2026-07-01T00:00:00Z`;
|
||||||
const dateRegex = /^(\d{4})-(\d{2})-(\d{2})([tT ])(\d{2}):(\d{2}):(\d{2})\.?(\d{3})?(?:(?:([+-]\d{2}):?(\d{2}))|Z)?$/;
|
const dateRegex = /^(\d{4})-(\d{2})-(\d{2})([tT ])(\d{2}):(\d{2}):(\d{2})\.?(\d{3})?(?:(?:([+-]\d{2}):?(\d{2}))|Z)?$/;
|
||||||
const formatRFC3339 = (date) => {
|
const formatRFC3339 = (date) => {
|
||||||
const pad = (num = 0) => `${+num < 10 ? 0 : ""}${+num}`;
|
const pad = (num = 0) => `${+num < 10 ? 0 : ""}${+num}`;
|
||||||
@@ -2928,10 +2929,14 @@ const formatRFC3339 = (date) => {
|
|||||||
offset
|
offset
|
||||||
].join("");
|
].join("");
|
||||||
};
|
};
|
||||||
function hashTwt(twt) {
|
function hashTwt(twt, version = 0) {
|
||||||
const created = formatRFC3339(twt.created);
|
const created = formatRFC3339(twt.created);
|
||||||
const payload = [twt.url, created, twt.content].join("\n");
|
const payload = [twt.url, created, twt.content].join("\n");
|
||||||
return base32Encode(blakejsExports.blake2b(payload, void 0, 32)).toLowerCase().slice(-7);
|
const encoded = base32Encode(blakejsExports.blake2b(payload, void 0, 32)).toLowerCase();
|
||||||
|
const createdDate = new Date(twt.created), epochDate = new Date(HASH_V2_EPOCH);
|
||||||
|
if (version === 1 || !version && createdDate < epochDate)
|
||||||
|
return encoded.slice(-7);
|
||||||
|
return encoded.slice(0, 12);
|
||||||
}
|
}
|
||||||
var dayjs_min$1 = { exports: {} };
|
var dayjs_min$1 = { exports: {} };
|
||||||
var dayjs_min = dayjs_min$1.exports;
|
var dayjs_min = dayjs_min$1.exports;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
800
dist-demo/dist-browser/twtxt-lib.min.js
vendored
800
dist-demo/dist-browser/twtxt-lib.min.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -14,8 +14,10 @@
|
|||||||
<ul class="tabs">
|
<ul class="tabs">
|
||||||
<li class="tab tabOverview" id="overview">
|
<li class="tab tabOverview" id="overview">
|
||||||
<a class="tab-link" href="#overview" id="tabOverview-link">Overview</a>
|
<a class="tab-link" href="#overview" id="tabOverview-link">Overview</a>
|
||||||
|
|
||||||
<div class="tab-panel" id="tabOverview-panel">
|
<div class="tab-panel" id="tabOverview-panel">
|
||||||
<h1>twtxt-lib</h2>
|
<h1>twtxt-lib</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
An isomorphic TypeScript library of
|
An isomorphic TypeScript library of
|
||||||
utility functions for parsing and interacting with
|
utility functions for parsing and interacting with
|
||||||
@@ -47,17 +49,28 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>Isomorphic, available as an
|
<li>Isomorphic, available as an
|
||||||
(<a href="/dist-browser/twtxt-lib.min.js">optionally minified</a>)
|
(<a href="/dist-browser/twtxt-lib.min.js">optionally minified</a>)
|
||||||
<a href="/dist-browser/twtxt-lib.js">ES6+ library for the browser</a>,
|
<a href="/dist-browser/twtxt-lib.js">ES6+ library for the browser</a>
|
||||||
with NPM and JSR versions coming soon.
|
or as a package (from
|
||||||
<li>Fully typed and source-mapped.</li>
|
<a href="https://www.npmjs.com/package/twtxt-lib">NPM</a> or
|
||||||
<li>Built as an <a href="https://caniuse.com/es6-module">ES6 module</a> (and <a href="https://antfu.me/posts/move-on-to-esm-only">ESM only</a>)</li>
|
<a href="https://jsr.io/@itsericwoodward/twtxt-lib">JSR</a>).
|
||||||
<li>Includes an interactive demo <em>(you're looking at it)</em>.</li>
|
</li>
|
||||||
|
|
||||||
|
<li>Fully typed and source-mapped (I hope).</li>
|
||||||
|
|
||||||
|
<li>Built as an <a href="https://caniuse.com/es6-module">ES6 module</a>
|
||||||
|
(and <a href="https://antfu.me/posts/move-on-to-esm-only">ESM only</a>)
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
Includes sample files and an interactive demo <em>(you're looking
|
||||||
|
at it)</em>.
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
<h2>Installation</h2>
|
<h2>Installation</h2>
|
||||||
|
|
||||||
<p>This library can be installed in several different ways:</p>
|
<p>This library can be installed several different ways:</p>
|
||||||
|
|
||||||
<h3>For the Browser</h3>
|
<h3>For the Browser</h3>
|
||||||
|
|
||||||
@@ -70,6 +83,7 @@
|
|||||||
<a href="/dist-browser/twtxt-lib.js">website</a>, or doing a
|
<a href="/dist-browser/twtxt-lib.js">website</a>, or doing a
|
||||||
<code>git clone https://git.itsericwoodward.com/eric/twtxt-lib.git</code>
|
<code>git clone https://git.itsericwoodward.com/eric/twtxt-lib.git</code>
|
||||||
and pulling it out of the <code>dist-browser</code> folder.
|
and pulling it out of the <code>dist-browser</code> folder.
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
Alternatively, you can grab the minified version from the
|
Alternatively, you can grab the minified version from the
|
||||||
@@ -78,10 +92,12 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
Add the newly acquired file to your static site / progressive web app /
|
Add the newly acquired file to your static site / progressive web app /
|
||||||
over-engineered blog.
|
over-engineered blog.
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
Import the desired function(s) via ESM:
|
Import the desired function(s) via ESM:
|
||||||
<code>import { hashTwt, loadAndParseTwtxtFile } from "./twtxt-lib.js";</code>
|
<code>import { hashTwt, loadAndParseTwtxtFile } from "./twtxt-lib.js";</code>
|
||||||
@@ -93,19 +109,22 @@
|
|||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>
|
||||||
Add the package to your project.
|
Add the package to your project.
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
Via <a href="https://www.npmjs.com/package/twtxt-lib">NPM</a>:
|
Via <a href="https://www.npmjs.com/package/twtxt-lib">NPM</a>:
|
||||||
<code>yarn add twtxt-lib</code>
|
<code>yarn add twtxt-lib</code>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
Via <a href="https://jsr.io/@itsericwoodward/twtxt-lib">JSR</a>:
|
Via <a href="https://jsr.io/@itsericwoodward/twtxt-lib">JSR</a>:
|
||||||
<code>yarn add jsr:@itsericwoodward/twtxt-lib</code>
|
<code>yarn add jsr:@itsericwoodward/twtxt-lib</code>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
Import the desired function(s) into your code:
|
Import the desired function(s) via ESM:
|
||||||
<code>import { hashTwt, loadAndParseTwtxtFile } from "twtxt-lib";</code>
|
<code>import { hashTwt, loadAndParseTwtxtFile } from "twtxt-lib";</code>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
@@ -121,16 +140,27 @@
|
|||||||
<code>/twtxt-demos/demo-hipster-twtxt.txt</code> </a
|
<code>/twtxt-demos/demo-hipster-twtxt.txt</code> </a
|
||||||
>:
|
>:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<span class="dotLoader"></span>
|
<span class="dotLoader"></span>
|
||||||
</div>
|
</div>
|
||||||
<p class="copyright">
|
|
||||||
Copyright © 2026 Eric Woodward, released under the <a href="https://www.itsericwoodward.com/licenses/mit/">MIT License</a>.
|
<p>
|
||||||
</p>
|
Example files generated with
|
||||||
|
<a href="https://hipsum.co/">Hipster Ipsum</a>,
|
||||||
|
<a href="https://pirateipsum.me/">Pirate Ipsum</a>,
|
||||||
|
a different <a href="https://lorem-ipsumm.com/pirate-ipsum/">Pirate Ipsum</a>, and
|
||||||
|
<a href="https://saganipsum.com/">Sagan Ipsum</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="copyright">
|
||||||
|
Copyright © 2026 Eric Woodward, released under the <a href="https://www.itsericwoodward.com/licenses/mit/">MIT License</a>.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="tab tabHashTwt" id="hashTwt">
|
<li class="tab tabHashTwt" id="hashTwt">
|
||||||
<a class="tab-link" href="#hashTwt">hashTwt</a>
|
<a class="tab-link" href="#hashTwt">hashTwt</a>
|
||||||
|
|
||||||
<div class="tab-panel" id="tabHashTwt-panel">
|
<div class="tab-panel" id="tabHashTwt-panel">
|
||||||
<p>
|
<p>
|
||||||
A function that takes the constituent parts of a “twt” and
|
A function that takes the constituent parts of a “twt” and
|
||||||
@@ -138,15 +168,43 @@
|
|||||||
<a href="https://twtxt.dev/exts/twt-hash.html">extension-compatible hash</a>
|
<a href="https://twtxt.dev/exts/twt-hash.html">extension-compatible hash</a>
|
||||||
for it.
|
for it.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Version 0.10.0 and above includes support for
|
||||||
|
<a href="https://git.mills.io/yarnsocial/twtxt.dev/pulls/28">V2 of the
|
||||||
|
Hashing Spec</a>:
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>A specific hashing version can be provided as an optional argument.</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
When no version argument is provided, it defaults to using version
|
||||||
|
1 for all twts with a created date before the epoch date
|
||||||
|
(<code>2026-07-01T00:00:00Z</code>), and version 2 for all twts
|
||||||
|
created on or after the epoch.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
|
||||||
<form id="formHash" name="formHash" method="post">
|
<form id="formHash" name="formHash" method="post">
|
||||||
<div class="flexRow">
|
<div class="flexRow">
|
||||||
<div class="flexCol">
|
<div class="flexCol">
|
||||||
|
<label for="version">Version
|
||||||
|
<select id="version" name="version">
|
||||||
|
<option value="0">Default</option>
|
||||||
|
<option value="1">v1</option>
|
||||||
|
<option value="2">v2</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
<label for="content">Content</label>
|
<label for="content">Content</label>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
id="content"
|
id="content"
|
||||||
name="content"
|
name="content"
|
||||||
>Prow scuttle parley provost Sail ho shrouds spirits boom mizzenmast yardarm.</textarea>
|
>Prow scuttle parley provost Sail ho shrouds spirits boom mizzenmast yardarm.</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flexCol">
|
<div class="flexCol">
|
||||||
<label>
|
<label>
|
||||||
Created
|
Created
|
||||||
@@ -156,6 +214,7 @@
|
|||||||
value="2026-02-01T01:23:45Z"
|
value="2026-02-01T01:23:45Z"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
URL
|
URL
|
||||||
<input
|
<input
|
||||||
@@ -164,6 +223,7 @@
|
|||||||
value="https://example.org/~pirate/twtxt.txt"
|
value="https://example.org/~pirate/twtxt.txt"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input type="submit" value="Go" />
|
<input type="submit" value="Go" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -173,12 +233,12 @@
|
|||||||
|
|
||||||
<li class="tab tabParse" id="parseTwtxt">
|
<li class="tab tabParse" id="parseTwtxt">
|
||||||
<a class="tab-link" href="#parseTwtxt">parseTwtxt</a>
|
<a class="tab-link" href="#parseTwtxt">parseTwtxt</a>
|
||||||
|
|
||||||
<div class="tab-panel" id="tabParse-panel">
|
<div class="tab-panel" id="tabParse-panel">
|
||||||
<p>
|
<p>
|
||||||
A function that parses a twtxt file string,
|
A function that parses a twtxt file string,
|
||||||
returning an object with information about the file and its owner
|
returning an object with information about the file and its owner
|
||||||
(including <a href="https://twtxt.dev/exts/twt-hash.html">hashes</a>
|
(including <a href="#hashTwt">hashes</a> for each twt and any
|
||||||
for each twt and any
|
|
||||||
<a href="https://twtxt.dev/exts/metadata.html">metadata</a> in the
|
<a href="https://twtxt.dev/exts/metadata.html">metadata</a> in the
|
||||||
file).
|
file).
|
||||||
</p>
|
</p>
|
||||||
@@ -187,6 +247,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<code>/twtxt-demos/demo-hipster-twtxt.txt</code>
|
<code>/twtxt-demos/demo-hipster-twtxt.txt</code>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
data-url="/twtxt-demos/demo-hipster-twtxt.txt"
|
data-url="/twtxt-demos/demo-hipster-twtxt.txt"
|
||||||
id="parseHipsterButton"
|
id="parseHipsterButton"
|
||||||
@@ -195,8 +256,10 @@
|
|||||||
Load
|
Load
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<code>/twtxt-demos/demo-pirate-twtxt.txt</code>
|
<code>/twtxt-demos/demo-pirate-twtxt.txt</code>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
data-url="/twtxt-demos/demo-pirate-twtxt.txt"
|
data-url="/twtxt-demos/demo-pirate-twtxt.txt"
|
||||||
id="parsePirateButton"
|
id="parsePirateButton"
|
||||||
@@ -205,8 +268,10 @@
|
|||||||
Load
|
Load
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<code>/twtxt-demos/demo-sagan-twtxt.txt</code>
|
<code>/twtxt-demos/demo-sagan-twtxt.txt</code>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
data-url="/twtxt-demos/demo-sagan-twtxt.txt"
|
data-url="/twtxt-demos/demo-sagan-twtxt.txt"
|
||||||
id="parseSaganButton"
|
id="parseSaganButton"
|
||||||
@@ -216,13 +281,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
|
||||||
<em
|
|
||||||
>Note that CORS restrictions may limit the
|
|
||||||
effectiveness of using this function from another
|
|
||||||
domain.</em
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form id="formParse" name="formParse" method="post">
|
<form id="formParse" name="formParse" method="post">
|
||||||
<div class="flexRow">
|
<div class="flexRow">
|
||||||
@@ -236,7 +294,9 @@
|
|||||||
value="/twtxt-demos/demo-hipster-twtxt.txt"
|
value="/twtxt-demos/demo-hipster-twtxt.txt"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input type="submit" value="Go" />
|
<input type="submit" value="Go" />
|
||||||
|
|
||||||
<span class="dotLoader"></span>
|
<span class="dotLoader"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -246,15 +306,19 @@
|
|||||||
|
|
||||||
<li class="tab tabLoadAndParse" id="loadAndParseTwtxtFile">
|
<li class="tab tabLoadAndParse" id="loadAndParseTwtxtFile">
|
||||||
<a class="tab-link" href="#loadAndParseTwtxtFile">loadAndParseTwtxtFile</a>
|
<a class="tab-link" href="#loadAndParseTwtxtFile">loadAndParseTwtxtFile</a>
|
||||||
|
|
||||||
<div class="tab-panel" id="tabLoadAndParse-panel">
|
<div class="tab-panel" id="tabLoadAndParse-panel">
|
||||||
<p>
|
<p>
|
||||||
An async function that fetches a twtxt.txt-compatible file
|
An async function that fetches a <code>twtxt.txt</code>-compatible file
|
||||||
from a URL and parses it into an object
|
from a URL and parses it, returning the extracted data as an object.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>Pre-included examples:</p>
|
<p>Pre-included examples:</p>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<code>/twtxt-demos/demo-hipster-twtxt.txt</code>
|
<code>/twtxt-demos/demo-hipster-twtxt.txt</code>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
data-url="/twtxt-demos/demo-hipster-twtxt.txt"
|
data-url="/twtxt-demos/demo-hipster-twtxt.txt"
|
||||||
id="loadAndParseHipsterButton"
|
id="loadAndParseHipsterButton"
|
||||||
@@ -263,8 +327,10 @@
|
|||||||
Load
|
Load
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<code>/twtxt-demos/demo-pirate-twtxt.txt</code>
|
<code>/twtxt-demos/demo-pirate-twtxt.txt</code>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
data-url="/twtxt-demos/demo-pirate-twtxt.txt"
|
data-url="/twtxt-demos/demo-pirate-twtxt.txt"
|
||||||
id="loadAndParsePirateButton"
|
id="loadAndParsePirateButton"
|
||||||
@@ -273,8 +339,10 @@
|
|||||||
Load
|
Load
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<code>/twtxt-demos/demo-sagan-twtxt.txt</code>
|
<code>/twtxt-demos/demo-sagan-twtxt.txt</code>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
data-url="/twtxt-demos/demo-sagan-twtxt.txt"
|
data-url="/twtxt-demos/demo-sagan-twtxt.txt"
|
||||||
id="loadAndParseSaganButton"
|
id="loadAndParseSaganButton"
|
||||||
@@ -284,12 +352,14 @@
|
|||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<em
|
<em>
|
||||||
>Note that CORS restrictions may limit the
|
Note that
|
||||||
effectiveness of using this function from another
|
<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS">Cross-Origin
|
||||||
domain.</em
|
Resource Sharing (CORS)</a> restrictions may limit the
|
||||||
>
|
effectiveness of using this function across domains.
|
||||||
|
</em>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
@@ -308,7 +378,9 @@
|
|||||||
value="/twtxt-demos/demo-hipster-twtxt.txt"
|
value="/twtxt-demos/demo-hipster-twtxt.txt"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input type="submit" value="Go" />
|
<input type="submit" value="Go" />
|
||||||
|
|
||||||
<span class="dotLoader"></span>
|
<span class="dotLoader"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
2
dist-node/constants.d.ts
vendored
2
dist-node/constants.d.ts
vendored
@@ -1 +1 @@
|
|||||||
export declare const __dirname: string;
|
export declare const HASH_V2_EPOCH = "2026-07-01T00:00:00Z";
|
||||||
|
|||||||
5
dist-node/constants.js
Normal file
5
dist-node/constants.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const HASH_V2_EPOCH = `2026-07-01T00:00:00Z`;
|
||||||
|
export {
|
||||||
|
HASH_V2_EPOCH
|
||||||
|
};
|
||||||
|
//# sourceMappingURL=constants.js.map
|
||||||
1
dist-node/constants.js.map
Normal file
1
dist-node/constants.js.map
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"constants.js","sources":["../src/constants.ts"],"sourcesContent":["export const HASH_V2_EPOCH = `2026-07-01T00:00:00Z`;\n"],"names":[],"mappings":"AAAO,MAAM,gBAAgB;"}
|
||||||
3
dist-node/hashTwt.d.ts
vendored
3
dist-node/hashTwt.d.ts
vendored
@@ -1,2 +1,3 @@
|
|||||||
import { Twt } from './types.ts';
|
import { Twt } from './types.ts';
|
||||||
export default function hashTwt(twt: Twt): string;
|
export type HashableTwt = Pick<Twt, "content" | "created" | "url"> & Partial<Omit<Twt, "content" | "created" | "url">>;
|
||||||
|
export default function hashTwt(twt: HashableTwt, version?: number): string;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { blake2b } from "@exodus/blakejs";
|
import { blake2b } from "@exodus/blakejs";
|
||||||
import { base32Encode } from "./utils.js";
|
import { base32Encode } from "./utils.js";
|
||||||
|
import { HASH_V2_EPOCH } from "./constants.js";
|
||||||
const dateRegex = /^(\d{4})-(\d{2})-(\d{2})([tT ])(\d{2}):(\d{2}):(\d{2})\.?(\d{3})?(?:(?:([+-]\d{2}):?(\d{2}))|Z)?$/;
|
const dateRegex = /^(\d{4})-(\d{2})-(\d{2})([tT ])(\d{2}):(\d{2}):(\d{2})\.?(\d{3})?(?:(?:([+-]\d{2}):?(\d{2}))|Z)?$/;
|
||||||
const formatRFC3339 = (date) => {
|
const formatRFC3339 = (date) => {
|
||||||
const pad = (num = 0) => `${+num < 10 ? 0 : ""}${+num}`;
|
const pad = (num = 0) => `${+num < 10 ? 0 : ""}${+num}`;
|
||||||
@@ -28,10 +29,14 @@ const formatRFC3339 = (date) => {
|
|||||||
offset
|
offset
|
||||||
].join("");
|
].join("");
|
||||||
};
|
};
|
||||||
function hashTwt(twt) {
|
function hashTwt(twt, version = 0) {
|
||||||
const created = formatRFC3339(twt.created);
|
const created = formatRFC3339(twt.created);
|
||||||
const payload = [twt.url, created, twt.content].join("\n");
|
const payload = [twt.url, created, twt.content].join("\n");
|
||||||
return base32Encode(blake2b(payload, void 0, 32)).toLowerCase().slice(-7);
|
const encoded = base32Encode(blake2b(payload, void 0, 32)).toLowerCase();
|
||||||
|
const createdDate = new Date(twt.created), epochDate = new Date(HASH_V2_EPOCH);
|
||||||
|
if (version === 1 || !version && createdDate < epochDate)
|
||||||
|
return encoded.slice(-7);
|
||||||
|
return encoded.slice(0, 12);
|
||||||
}
|
}
|
||||||
export {
|
export {
|
||||||
hashTwt as default
|
hashTwt as default
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"hashTwt.js","sources":["../src/hashTwt.ts"],"sourcesContent":["import type { Twt } from \"./types.ts\";\n\nimport { blake2b } from \"@exodus/blakejs\";\n\nimport { base32Encode } from \"./utils.ts\";\n\nconst dateRegex =\n\t/^(\\d{4})-(\\d{2})-(\\d{2})([tT ])(\\d{2}):(\\d{2}):(\\d{2})\\.?(\\d{3})?(?:(?:([+-]\\d{2}):?(\\d{2}))|Z)?$/;\n\nconst formatRFC3339 = (date: string) => {\n\tconst pad = (num: number | string = 0) => `${+num < 10 ? 0 : \"\"}${+num}`;\n\tconst padYear = (num: number | string = 0) =>\n\t\t`${+num < 1000 ? 0 : \"\"}${+num < 100 ? 0 : \"\"}${\n\t\t\t+num < 10 ? 0 : \"\"\n\t\t}${+num}`;\n\n\tlet m = dateRegex.exec(date);\n\n\t//if timezone is undefined, it must be Z or nothing (otherwise the group would have captured).\n\tif (m && m?.[9] === undefined) {\n\t\t//Use UTC.\n\t\tm[9] = \"+00\";\n\t}\n\tif (m && m?.[10] === undefined) {\n\t\tm[10] = \"00\";\n\t}\n\n\tconst offset = `${m?.[9]}:${m?.[10]}`.replace(/[+-]?00:00$/, \"Z\");\n\n\treturn [\n\t\tpadYear(m?.[1]),\n\t\t\"-\",\n\t\tpad(m?.[2]),\n\t\t\"-\",\n\t\tpad(m?.[3]),\n\t\tm?.[4],\n\t\tpad(m?.[5]),\n\t\t\":\",\n\t\tpad(m?.[6]),\n\t\t\":\",\n\t\tpad(m?.[7]),\n\t\t//ignore milliseconds (m[8])\n\t\toffset,\n\t].join(\"\");\n};\n\nexport default function hashTwt(twt: Twt): string {\n\tconst created = formatRFC3339(twt.created);\n\tconst payload = [twt.url, created, twt.content].join(\"\\n\");\n\n\treturn base32Encode(blake2b(payload, undefined, 32))\n\t\t.toLowerCase()\n\t\t.slice(-7);\n}\n"],"names":[],"mappings":";;AAMA,MAAM,YACL;AAED,MAAM,gBAAgB,CAAC,SAAiB;AACvC,QAAM,MAAM,CAAC,MAAuB,MAAM,GAAG,CAAC,MAAM,KAAK,IAAI,EAAE,GAAG,CAAC,GAAG;AACtE,QAAM,UAAU,CAAC,MAAuB,MACvC,GAAG,CAAC,MAAM,MAAO,IAAI,EAAE,GAAG,CAAC,MAAM,MAAM,IAAI,EAAE,GAC5C,CAAC,MAAM,KAAK,IAAI,EACjB,GAAG,CAAC,GAAG;AAER,MAAI,IAAI,UAAU,KAAK,IAAI;AAG3B,MAAI,KAAK,IAAI,CAAC,MAAM,QAAW;AAE9B,MAAE,CAAC,IAAI;AAAA,EACR;AACA,MAAI,KAAK,IAAI,EAAE,MAAM,QAAW;AAC/B,MAAE,EAAE,IAAI;AAAA,EACT;AAEA,QAAM,SAAS,GAAG,IAAI,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,GAAG,QAAQ,eAAe,GAAG;AAEhE,SAAO;AAAA,IACN,QAAQ,IAAI,CAAC,CAAC;AAAA,IACd;AAAA,IACA,IAAI,IAAI,CAAC,CAAC;AAAA,IACV;AAAA,IACA,IAAI,IAAI,CAAC,CAAC;AAAA,IACV,IAAI,CAAC;AAAA,IACL,IAAI,IAAI,CAAC,CAAC;AAAA,IACV;AAAA,IACA,IAAI,IAAI,CAAC,CAAC;AAAA,IACV;AAAA,IACA,IAAI,IAAI,CAAC,CAAC;AAAA;AAAA,IAEV;AAAA,EAAA,EACC,KAAK,EAAE;AACV;AAEA,SAAwB,QAAQ,KAAkB;AACjD,QAAM,UAAU,cAAc,IAAI,OAAO;AACzC,QAAM,UAAU,CAAC,IAAI,KAAK,SAAS,IAAI,OAAO,EAAE,KAAK,IAAI;AAEzD,SAAO,aAAa,QAAQ,SAAS,QAAW,EAAE,CAAC,EACjD,YAAA,EACA,MAAM,EAAE;AACX;"}
|
{"version":3,"file":"hashTwt.js","sources":["../src/hashTwt.ts"],"sourcesContent":["import type { Twt } from \"./types.ts\";\n\nimport { blake2b } from \"@exodus/blakejs\";\n\nimport { base32Encode } from \"./utils.ts\";\nimport { HASH_V2_EPOCH } from \"./constants.ts\";\n\nconst dateRegex =\n\t/^(\\d{4})-(\\d{2})-(\\d{2})([tT ])(\\d{2}):(\\d{2}):(\\d{2})\\.?(\\d{3})?(?:(?:([+-]\\d{2}):?(\\d{2}))|Z)?$/;\n\nconst formatRFC3339 = (date: string) => {\n\tconst pad = (num: number | string = 0) => `${+num < 10 ? 0 : \"\"}${+num}`;\n\tconst padYear = (num: number | string = 0) =>\n\t\t`${+num < 1000 ? 0 : \"\"}${+num < 100 ? 0 : \"\"}${\n\t\t\t+num < 10 ? 0 : \"\"\n\t\t}${+num}`;\n\n\tlet m = dateRegex.exec(date);\n\n\t//if timezone is undefined, it must be Z or nothing (otherwise the group would have captured).\n\tif (m && m?.[9] === undefined) {\n\t\t//Use UTC.\n\t\tm[9] = \"+00\";\n\t}\n\tif (m && m?.[10] === undefined) {\n\t\tm[10] = \"00\";\n\t}\n\n\tconst offset = `${m?.[9]}:${m?.[10]}`.replace(/[+-]?00:00$/, \"Z\");\n\n\treturn [\n\t\tpadYear(m?.[1]),\n\t\t\"-\",\n\t\tpad(m?.[2]),\n\t\t\"-\",\n\t\tpad(m?.[3]),\n\t\tm?.[4],\n\t\tpad(m?.[5]),\n\t\t\":\",\n\t\tpad(m?.[6]),\n\t\t\":\",\n\t\tpad(m?.[7]),\n\t\t//ignore milliseconds (m[8])\n\t\toffset,\n\t].join(\"\");\n};\n\nexport type HashableTwt = Pick<Twt, \"content\" | \"created\" | \"url\"> &\n\tPartial<Omit<Twt, \"content\" | \"created\" | \"url\">>;\n\nexport default function hashTwt(twt: HashableTwt, version = 0): string {\n\tconst created = formatRFC3339(twt.created);\n\tconst payload = [twt.url, created, twt.content].join(\"\\n\");\n\n\tconst encoded = base32Encode(blake2b(payload, undefined, 32)).toLowerCase();\n\n\tconst createdDate = new Date(twt.created),\n\t\tepochDate = new Date(HASH_V2_EPOCH);\n\n\tif (version === 1 || (!version && createdDate < epochDate))\n\t\treturn encoded.slice(-7);\n\treturn encoded.slice(0, 12);\n}\n"],"names":[],"mappings":";;;AAOA,MAAM,YACL;AAED,MAAM,gBAAgB,CAAC,SAAiB;AACvC,QAAM,MAAM,CAAC,MAAuB,MAAM,GAAG,CAAC,MAAM,KAAK,IAAI,EAAE,GAAG,CAAC,GAAG;AACtE,QAAM,UAAU,CAAC,MAAuB,MACvC,GAAG,CAAC,MAAM,MAAO,IAAI,EAAE,GAAG,CAAC,MAAM,MAAM,IAAI,EAAE,GAC5C,CAAC,MAAM,KAAK,IAAI,EACjB,GAAG,CAAC,GAAG;AAER,MAAI,IAAI,UAAU,KAAK,IAAI;AAG3B,MAAI,KAAK,IAAI,CAAC,MAAM,QAAW;AAE9B,MAAE,CAAC,IAAI;AAAA,EACR;AACA,MAAI,KAAK,IAAI,EAAE,MAAM,QAAW;AAC/B,MAAE,EAAE,IAAI;AAAA,EACT;AAEA,QAAM,SAAS,GAAG,IAAI,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,GAAG,QAAQ,eAAe,GAAG;AAEhE,SAAO;AAAA,IACN,QAAQ,IAAI,CAAC,CAAC;AAAA,IACd;AAAA,IACA,IAAI,IAAI,CAAC,CAAC;AAAA,IACV;AAAA,IACA,IAAI,IAAI,CAAC,CAAC;AAAA,IACV,IAAI,CAAC;AAAA,IACL,IAAI,IAAI,CAAC,CAAC;AAAA,IACV;AAAA,IACA,IAAI,IAAI,CAAC,CAAC;AAAA,IACV;AAAA,IACA,IAAI,IAAI,CAAC,CAAC;AAAA;AAAA,IAEV;AAAA,EAAA,EACC,KAAK,EAAE;AACV;AAKA,SAAwB,QAAQ,KAAkB,UAAU,GAAW;AACtE,QAAM,UAAU,cAAc,IAAI,OAAO;AACzC,QAAM,UAAU,CAAC,IAAI,KAAK,SAAS,IAAI,OAAO,EAAE,KAAK,IAAI;AAEzD,QAAM,UAAU,aAAa,QAAQ,SAAS,QAAW,EAAE,CAAC,EAAE,YAAA;AAE9D,QAAM,cAAc,IAAI,KAAK,IAAI,OAAO,GACvC,YAAY,IAAI,KAAK,aAAa;AAEnC,MAAI,YAAY,KAAM,CAAC,WAAW,cAAc;AAC/C,WAAO,QAAQ,MAAM,EAAE;AACxB,SAAO,QAAQ,MAAM,GAAG,EAAE;AAC3B;"}
|
||||||
126
index.html
126
index.html
@@ -14,8 +14,10 @@
|
|||||||
<ul class="tabs">
|
<ul class="tabs">
|
||||||
<li class="tab tabOverview" id="overview">
|
<li class="tab tabOverview" id="overview">
|
||||||
<a class="tab-link" href="#overview" id="tabOverview-link">Overview</a>
|
<a class="tab-link" href="#overview" id="tabOverview-link">Overview</a>
|
||||||
|
|
||||||
<div class="tab-panel" id="tabOverview-panel">
|
<div class="tab-panel" id="tabOverview-panel">
|
||||||
<h1>twtxt-lib</h2>
|
<h1>twtxt-lib</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
An isomorphic TypeScript library of
|
An isomorphic TypeScript library of
|
||||||
utility functions for parsing and interacting with
|
utility functions for parsing and interacting with
|
||||||
@@ -47,17 +49,28 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>Isomorphic, available as an
|
<li>Isomorphic, available as an
|
||||||
(<a href="/dist-browser/twtxt-lib.min.js">optionally minified</a>)
|
(<a href="/dist-browser/twtxt-lib.min.js">optionally minified</a>)
|
||||||
<a href="/dist-browser/twtxt-lib.js">ES6+ library for the browser</a>,
|
<a href="/dist-browser/twtxt-lib.js">ES6+ library for the browser</a>
|
||||||
with NPM and JSR versions coming soon.
|
or as a package (from
|
||||||
<li>Fully typed and source-mapped.</li>
|
<a href="https://www.npmjs.com/package/twtxt-lib">NPM</a> or
|
||||||
<li>Built as an <a href="https://caniuse.com/es6-module">ES6 module</a> (and <a href="https://antfu.me/posts/move-on-to-esm-only">ESM only</a>)</li>
|
<a href="https://jsr.io/@itsericwoodward/twtxt-lib">JSR</a>).
|
||||||
<li>Includes an interactive demo <em>(you're looking at it)</em>.</li>
|
</li>
|
||||||
|
|
||||||
|
<li>Fully typed and source-mapped (I hope).</li>
|
||||||
|
|
||||||
|
<li>Built as an <a href="https://caniuse.com/es6-module">ES6 module</a>
|
||||||
|
(and <a href="https://antfu.me/posts/move-on-to-esm-only">ESM only</a>)
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
Includes sample files and an interactive demo <em>(you're looking
|
||||||
|
at it)</em>.
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
<h2>Installation</h2>
|
<h2>Installation</h2>
|
||||||
|
|
||||||
<p>This library can be installed in several different ways:</p>
|
<p>This library can be installed several different ways:</p>
|
||||||
|
|
||||||
<h3>For the Browser</h3>
|
<h3>For the Browser</h3>
|
||||||
|
|
||||||
@@ -70,6 +83,7 @@
|
|||||||
<a href="/dist-browser/twtxt-lib.js">website</a>, or doing a
|
<a href="/dist-browser/twtxt-lib.js">website</a>, or doing a
|
||||||
<code>git clone https://git.itsericwoodward.com/eric/twtxt-lib.git</code>
|
<code>git clone https://git.itsericwoodward.com/eric/twtxt-lib.git</code>
|
||||||
and pulling it out of the <code>dist-browser</code> folder.
|
and pulling it out of the <code>dist-browser</code> folder.
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
Alternatively, you can grab the minified version from the
|
Alternatively, you can grab the minified version from the
|
||||||
@@ -78,10 +92,12 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
Add the newly acquired file to your static site / progressive web app /
|
Add the newly acquired file to your static site / progressive web app /
|
||||||
over-engineered blog.
|
over-engineered blog.
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
Import the desired function(s) via ESM:
|
Import the desired function(s) via ESM:
|
||||||
<code>import { hashTwt, loadAndParseTwtxtFile } from "./twtxt-lib.js";</code>
|
<code>import { hashTwt, loadAndParseTwtxtFile } from "./twtxt-lib.js";</code>
|
||||||
@@ -93,19 +109,22 @@
|
|||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>
|
||||||
Add the package to your project.
|
Add the package to your project.
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
Via <a href="https://www.npmjs.com/package/twtxt-lib">NPM</a>:
|
Via <a href="https://www.npmjs.com/package/twtxt-lib">NPM</a>:
|
||||||
<code>yarn add twtxt-lib</code>
|
<code>yarn add twtxt-lib</code>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
Via <a href="https://jsr.io/@itsericwoodward/twtxt-lib">JSR</a>:
|
Via <a href="https://jsr.io/@itsericwoodward/twtxt-lib">JSR</a>:
|
||||||
<code>yarn add jsr:@itsericwoodward/twtxt-lib</code>
|
<code>yarn add jsr:@itsericwoodward/twtxt-lib</code>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
Import the desired function(s) into your code:
|
Import the desired function(s) via ESM:
|
||||||
<code>import { hashTwt, loadAndParseTwtxtFile } from "twtxt-lib";</code>
|
<code>import { hashTwt, loadAndParseTwtxtFile } from "twtxt-lib";</code>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
@@ -121,16 +140,27 @@
|
|||||||
<code>/twtxt-demos/demo-hipster-twtxt.txt</code> </a
|
<code>/twtxt-demos/demo-hipster-twtxt.txt</code> </a
|
||||||
>:
|
>:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<span class="dotLoader"></span>
|
<span class="dotLoader"></span>
|
||||||
</div>
|
</div>
|
||||||
<p class="copyright">
|
|
||||||
Copyright © 2026 Eric Woodward, released under the <a href="https://www.itsericwoodward.com/licenses/mit/">MIT License</a>.
|
<p>
|
||||||
</p>
|
Example files generated with
|
||||||
|
<a href="https://hipsum.co/">Hipster Ipsum</a>,
|
||||||
|
<a href="https://pirateipsum.me/">Pirate Ipsum</a>,
|
||||||
|
a different <a href="https://lorem-ipsumm.com/pirate-ipsum/">Pirate Ipsum</a>, and
|
||||||
|
<a href="https://saganipsum.com/">Sagan Ipsum</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="copyright">
|
||||||
|
Copyright © 2026 Eric Woodward, released under the <a href="https://www.itsericwoodward.com/licenses/mit/">MIT License</a>.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="tab tabHashTwt" id="hashTwt">
|
<li class="tab tabHashTwt" id="hashTwt">
|
||||||
<a class="tab-link" href="#hashTwt">hashTwt</a>
|
<a class="tab-link" href="#hashTwt">hashTwt</a>
|
||||||
|
|
||||||
<div class="tab-panel" id="tabHashTwt-panel">
|
<div class="tab-panel" id="tabHashTwt-panel">
|
||||||
<p>
|
<p>
|
||||||
A function that takes the constituent parts of a “twt” and
|
A function that takes the constituent parts of a “twt” and
|
||||||
@@ -138,15 +168,43 @@
|
|||||||
<a href="https://twtxt.dev/exts/twt-hash.html">extension-compatible hash</a>
|
<a href="https://twtxt.dev/exts/twt-hash.html">extension-compatible hash</a>
|
||||||
for it.
|
for it.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Version 0.10.0 and above includes support for
|
||||||
|
<a href="https://git.mills.io/yarnsocial/twtxt.dev/pulls/28">V2 of the
|
||||||
|
Hashing Spec</a>:
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>A specific hashing version can be provided as an optional argument.</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
When no version argument is provided, it defaults to using version
|
||||||
|
1 for all twts with a created date before the epoch date
|
||||||
|
(<code>2026-07-01T00:00:00Z</code>), and version 2 for all twts
|
||||||
|
created on or after the epoch.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
|
||||||
<form id="formHash" name="formHash" method="post">
|
<form id="formHash" name="formHash" method="post">
|
||||||
<div class="flexRow">
|
<div class="flexRow">
|
||||||
<div class="flexCol">
|
<div class="flexCol">
|
||||||
|
<label for="version">Version
|
||||||
|
<select id="version" name="version">
|
||||||
|
<option value="0">Default</option>
|
||||||
|
<option value="1">v1</option>
|
||||||
|
<option value="2">v2</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
<label for="content">Content</label>
|
<label for="content">Content</label>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
id="content"
|
id="content"
|
||||||
name="content"
|
name="content"
|
||||||
>Prow scuttle parley provost Sail ho shrouds spirits boom mizzenmast yardarm.</textarea>
|
>Prow scuttle parley provost Sail ho shrouds spirits boom mizzenmast yardarm.</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flexCol">
|
<div class="flexCol">
|
||||||
<label>
|
<label>
|
||||||
Created
|
Created
|
||||||
@@ -156,6 +214,7 @@
|
|||||||
value="2026-02-01T01:23:45Z"
|
value="2026-02-01T01:23:45Z"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
URL
|
URL
|
||||||
<input
|
<input
|
||||||
@@ -164,6 +223,7 @@
|
|||||||
value="https://example.org/~pirate/twtxt.txt"
|
value="https://example.org/~pirate/twtxt.txt"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input type="submit" value="Go" />
|
<input type="submit" value="Go" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -173,12 +233,12 @@
|
|||||||
|
|
||||||
<li class="tab tabParse" id="parseTwtxt">
|
<li class="tab tabParse" id="parseTwtxt">
|
||||||
<a class="tab-link" href="#parseTwtxt">parseTwtxt</a>
|
<a class="tab-link" href="#parseTwtxt">parseTwtxt</a>
|
||||||
|
|
||||||
<div class="tab-panel" id="tabParse-panel">
|
<div class="tab-panel" id="tabParse-panel">
|
||||||
<p>
|
<p>
|
||||||
A function that parses a twtxt file string,
|
A function that parses a twtxt file string,
|
||||||
returning an object with information about the file and its owner
|
returning an object with information about the file and its owner
|
||||||
(including <a href="https://twtxt.dev/exts/twt-hash.html">hashes</a>
|
(including <a href="#hashTwt">hashes</a> for each twt and any
|
||||||
for each twt and any
|
|
||||||
<a href="https://twtxt.dev/exts/metadata.html">metadata</a> in the
|
<a href="https://twtxt.dev/exts/metadata.html">metadata</a> in the
|
||||||
file).
|
file).
|
||||||
</p>
|
</p>
|
||||||
@@ -187,6 +247,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<code>/twtxt-demos/demo-hipster-twtxt.txt</code>
|
<code>/twtxt-demos/demo-hipster-twtxt.txt</code>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
data-url="/twtxt-demos/demo-hipster-twtxt.txt"
|
data-url="/twtxt-demos/demo-hipster-twtxt.txt"
|
||||||
id="parseHipsterButton"
|
id="parseHipsterButton"
|
||||||
@@ -195,8 +256,10 @@
|
|||||||
Load
|
Load
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<code>/twtxt-demos/demo-pirate-twtxt.txt</code>
|
<code>/twtxt-demos/demo-pirate-twtxt.txt</code>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
data-url="/twtxt-demos/demo-pirate-twtxt.txt"
|
data-url="/twtxt-demos/demo-pirate-twtxt.txt"
|
||||||
id="parsePirateButton"
|
id="parsePirateButton"
|
||||||
@@ -205,8 +268,10 @@
|
|||||||
Load
|
Load
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<code>/twtxt-demos/demo-sagan-twtxt.txt</code>
|
<code>/twtxt-demos/demo-sagan-twtxt.txt</code>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
data-url="/twtxt-demos/demo-sagan-twtxt.txt"
|
data-url="/twtxt-demos/demo-sagan-twtxt.txt"
|
||||||
id="parseSaganButton"
|
id="parseSaganButton"
|
||||||
@@ -216,13 +281,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
|
||||||
<em
|
|
||||||
>Note that CORS restrictions may limit the
|
|
||||||
effectiveness of using this function from another
|
|
||||||
domain.</em
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form id="formParse" name="formParse" method="post">
|
<form id="formParse" name="formParse" method="post">
|
||||||
<div class="flexRow">
|
<div class="flexRow">
|
||||||
@@ -236,7 +294,9 @@
|
|||||||
value="/twtxt-demos/demo-hipster-twtxt.txt"
|
value="/twtxt-demos/demo-hipster-twtxt.txt"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input type="submit" value="Go" />
|
<input type="submit" value="Go" />
|
||||||
|
|
||||||
<span class="dotLoader"></span>
|
<span class="dotLoader"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -246,15 +306,19 @@
|
|||||||
|
|
||||||
<li class="tab tabLoadAndParse" id="loadAndParseTwtxtFile">
|
<li class="tab tabLoadAndParse" id="loadAndParseTwtxtFile">
|
||||||
<a class="tab-link" href="#loadAndParseTwtxtFile">loadAndParseTwtxtFile</a>
|
<a class="tab-link" href="#loadAndParseTwtxtFile">loadAndParseTwtxtFile</a>
|
||||||
|
|
||||||
<div class="tab-panel" id="tabLoadAndParse-panel">
|
<div class="tab-panel" id="tabLoadAndParse-panel">
|
||||||
<p>
|
<p>
|
||||||
An async function that fetches a twtxt.txt-compatible file
|
An async function that fetches a <code>twtxt.txt</code>-compatible file
|
||||||
from a URL and parses it into an object
|
from a URL and parses it, returning the extracted data as an object.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>Pre-included examples:</p>
|
<p>Pre-included examples:</p>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<code>/twtxt-demos/demo-hipster-twtxt.txt</code>
|
<code>/twtxt-demos/demo-hipster-twtxt.txt</code>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
data-url="/twtxt-demos/demo-hipster-twtxt.txt"
|
data-url="/twtxt-demos/demo-hipster-twtxt.txt"
|
||||||
id="loadAndParseHipsterButton"
|
id="loadAndParseHipsterButton"
|
||||||
@@ -263,8 +327,10 @@
|
|||||||
Load
|
Load
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<code>/twtxt-demos/demo-pirate-twtxt.txt</code>
|
<code>/twtxt-demos/demo-pirate-twtxt.txt</code>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
data-url="/twtxt-demos/demo-pirate-twtxt.txt"
|
data-url="/twtxt-demos/demo-pirate-twtxt.txt"
|
||||||
id="loadAndParsePirateButton"
|
id="loadAndParsePirateButton"
|
||||||
@@ -273,8 +339,10 @@
|
|||||||
Load
|
Load
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<code>/twtxt-demos/demo-sagan-twtxt.txt</code>
|
<code>/twtxt-demos/demo-sagan-twtxt.txt</code>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
data-url="/twtxt-demos/demo-sagan-twtxt.txt"
|
data-url="/twtxt-demos/demo-sagan-twtxt.txt"
|
||||||
id="loadAndParseSaganButton"
|
id="loadAndParseSaganButton"
|
||||||
@@ -284,12 +352,14 @@
|
|||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<em
|
<em>
|
||||||
>Note that CORS restrictions may limit the
|
Note that
|
||||||
effectiveness of using this function from another
|
<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS">Cross-Origin
|
||||||
domain.</em
|
Resource Sharing (CORS)</a> restrictions may limit the
|
||||||
>
|
effectiveness of using this function across domains.
|
||||||
|
</em>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
@@ -308,7 +378,9 @@
|
|||||||
value="/twtxt-demos/demo-hipster-twtxt.txt"
|
value="/twtxt-demos/demo-hipster-twtxt.txt"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input type="submit" value="Go" />
|
<input type="submit" value="Go" />
|
||||||
|
|
||||||
<span class="dotLoader"></span>
|
<span class="dotLoader"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
2
jsr.json
2
jsr.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@itsericwoodward/twtxt-lib",
|
"name": "@itsericwoodward/twtxt-lib",
|
||||||
"version": "0.9.4",
|
"version": "0.10.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"exports": "./src/index.ts"
|
"exports": "./src/index.ts"
|
||||||
}
|
}
|
||||||
11
package.json
11
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "twtxt-lib",
|
"name": "twtxt-lib",
|
||||||
"version": "0.9.4",
|
"version": "0.10.0",
|
||||||
"description": "An isomorphic TypeScript library of utility functions for parsing and interacting with twtxt.txt files.",
|
"description": "An isomorphic TypeScript library of utility functions for parsing and interacting with twtxt.txt files.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": {
|
"author": {
|
||||||
@@ -33,7 +33,6 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"prepublishOnly": "yarn build",
|
"prepublishOnly": "yarn build",
|
||||||
"postpublish": "git push && git push --tags",
|
"postpublish": "git push && git push --tags",
|
||||||
"publish": "npm publish",
|
|
||||||
"publish:jsr": "npx jsr publish",
|
"publish:jsr": "npx jsr publish",
|
||||||
"publish:all": "npm publish && npx jsr publish",
|
"publish:all": "npm publish && npx jsr publish",
|
||||||
"test": "vitest"
|
"test": "vitest"
|
||||||
@@ -41,17 +40,17 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@exodus/blakejs": "^1.1.1-exodus.0",
|
"@exodus/blakejs": "^1.1.1-exodus.0",
|
||||||
"base32.js": "^0.1.0",
|
"base32.js": "^0.1.0",
|
||||||
"dayjs": "^1.11.19"
|
"dayjs": "^1.11.20"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^25.3.5",
|
"@types/node": "^25.5.0",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"unplugin-dts": "1.0.0-beta.6",
|
"unplugin-dts": "1.0.0-beta.6",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vite-plugin-no-bundle": "^4.0.0",
|
"vite-plugin-no-bundle": "^4.0.0",
|
||||||
"vite-plugin-node-polyfills": "^0.25.0",
|
"vite-plugin-node-polyfills": "^0.26.0",
|
||||||
"vitest": "^4.0.18"
|
"vitest": "^4.1.2"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.13.0"
|
"packageManager": "yarn@4.13.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,17 +9,22 @@ formHash.addEventListener("submit", (e) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const content = formHash.elements["content"].value;
|
const content = formHash.elements["content"].value;
|
||||||
const created = formHash.elements["created"].value;
|
const created = formHash.elements["created"].value;
|
||||||
|
const version = parseInt(formHash.elements["version"].value, 10);
|
||||||
const url = formHash.elements["url"].value;
|
const url = formHash.elements["url"].value;
|
||||||
const hash = hashTwt({
|
const hash = hashTwt(
|
||||||
content,
|
{
|
||||||
created,
|
content,
|
||||||
url,
|
created,
|
||||||
});
|
url,
|
||||||
|
},
|
||||||
|
version,
|
||||||
|
);
|
||||||
|
|
||||||
const result = [
|
const result = [
|
||||||
`content: ${content}`,
|
`content: ${content}`,
|
||||||
`created: ${created}`,
|
`created: ${created}`,
|
||||||
`url: ${url}`,
|
`url: ${url}`,
|
||||||
|
`version: ${version}`,
|
||||||
`hash: ${hash}`,
|
`hash: ${hash}`,
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
||||||
|
|||||||
@@ -15,18 +15,23 @@ formatSource(
|
|||||||
|
|
||||||
const content = formHash.elements["content"].value;
|
const content = formHash.elements["content"].value;
|
||||||
const created = formHash.elements["created"].value;
|
const created = formHash.elements["created"].value;
|
||||||
|
const version = parseInt(formHash.elements["version"].value, 10);
|
||||||
const url = formHash.elements["url"].value;
|
const url = formHash.elements["url"].value;
|
||||||
|
|
||||||
const hash = hashTwt({
|
const hash = hashTwt(
|
||||||
content,
|
{
|
||||||
created,
|
content,
|
||||||
url,
|
created,
|
||||||
});
|
url,
|
||||||
|
},
|
||||||
|
version,
|
||||||
|
);
|
||||||
|
|
||||||
const result = [
|
const result = [
|
||||||
\`content: \${content}\`,
|
\`content: \${content}\`,
|
||||||
\`created: \${created}\`,
|
\`created: \${created}\`,
|
||||||
\`url: \${url}\`,
|
\`url: \${url}\`,
|
||||||
|
\`version: \${version}\`,
|
||||||
\`hash: \${hash}\`,
|
\`hash: \${hash}\`,
|
||||||
].join("\\n");
|
].join("\\n");
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,18 @@ code {
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
details figure {
|
||||||
|
margin: 1rem .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
details figure pre {
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
details[open] summary ~ * {
|
||||||
|
animation: riseInDetails .5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -110,6 +122,15 @@ pre {
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
background-color: var(--fg-light);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
select:open {
|
||||||
|
background-color: var(--link-active);
|
||||||
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
background-color: var(--fg-light);
|
background-color: var(--fg-light);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
@@ -117,21 +138,6 @@ textarea {
|
|||||||
width: 100%;
|
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 {
|
summary {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,30 +3,195 @@ import { describe, expect, it } from "vitest";
|
|||||||
import hashTwt from "../hashTwt.ts";
|
import hashTwt from "../hashTwt.ts";
|
||||||
|
|
||||||
describe("hashtwt", () => {
|
describe("hashtwt", () => {
|
||||||
it("should correctly provide hash for known good twt", () => {
|
it("should correctly provide v1 hash for twt before epoch", () => {
|
||||||
// https://twtxt.net/twt/g524g5q
|
// https://twtxt.net/twt/g524g5q
|
||||||
const created = "2025-08-09T21:33:43-04:00";
|
|
||||||
const twt = {
|
const twt = {
|
||||||
content: "Is this thing on?",
|
content: "Is this thing on?",
|
||||||
created,
|
created: "2025-08-09T21:33:43-04:00",
|
||||||
createdUTC: "",
|
|
||||||
url: "http://itsericwoodward.com/twtxt.txt",
|
url: "http://itsericwoodward.com/twtxt.txt",
|
||||||
};
|
};
|
||||||
const result = hashTwt(twt);
|
const result = hashTwt(twt);
|
||||||
expect(result).toEqual("g524g5q");
|
expect(result).toEqual("g524g5q");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should correctly provide hash for twt", () => {
|
it("should correctly provide v1 hash for twt before epoch", () => {
|
||||||
const created = "2025-03-02T18:47:01+01:00";
|
|
||||||
// dayjs.utc(created).toISOString();
|
|
||||||
const twt = {
|
const twt = {
|
||||||
content:
|
content:
|
||||||
"Normcore tilde ad selfies, culpa cupping nostrud gatekeep aesthetic PBR&B 3 wolf moon mustache twee.",
|
"Normcore tilde ad selfies, culpa cupping nostrud gatekeep aesthetic PBR&B 3 wolf moon mustache twee.",
|
||||||
created,
|
created: "2025-03-02T18:47:01+01:00",
|
||||||
createdUTC: "",
|
|
||||||
url: "https://example.com/demo-hipster-twtxt.txt",
|
url: "https://example.com/demo-hipster-twtxt.txt",
|
||||||
};
|
};
|
||||||
const result = hashTwt(twt);
|
const result = hashTwt(twt);
|
||||||
expect(result).toEqual("tvjursa");
|
expect(result).toEqual("tvjursa");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should correctly provide v1 hash for twt before epoch", () => {
|
||||||
|
const twt = {
|
||||||
|
content: "Hello World!",
|
||||||
|
created: "2026-06-24T00:00:00Z",
|
||||||
|
url: "https://example.com/twtxt.txt",
|
||||||
|
};
|
||||||
|
const result = hashTwt(twt);
|
||||||
|
expect(result).toEqual("y34mvza");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly provide v1 hash for twt before epoch", () => {
|
||||||
|
const twt = {
|
||||||
|
content: "Hello World!",
|
||||||
|
created: "2026-06-30T23:59:57Z",
|
||||||
|
url: "https://example.com/twtxt.txt",
|
||||||
|
};
|
||||||
|
const result = hashTwt(twt);
|
||||||
|
expect(result).toEqual("5fket4q");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly provide v1 hash for twt before epoch", () => {
|
||||||
|
const twt = {
|
||||||
|
content: "Hello World!",
|
||||||
|
created: "2026-06-30T23:59:58Z",
|
||||||
|
url: "https://example.com/twtxt.txt",
|
||||||
|
};
|
||||||
|
const result = hashTwt(twt);
|
||||||
|
expect(result).toEqual("acwz4tq");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly provide v1 hash for twt before epoch", () => {
|
||||||
|
const twt = {
|
||||||
|
content: "Hello World!",
|
||||||
|
created: "2026-06-30T23:59:59Z",
|
||||||
|
url: "https://example.com/twtxt.txt",
|
||||||
|
};
|
||||||
|
const result = hashTwt(twt);
|
||||||
|
expect(result).toEqual("74qi5kq");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly provide v2 hash for twt after epoch", () => {
|
||||||
|
const twt = {
|
||||||
|
content: "Hello World!",
|
||||||
|
created: "2026-07-01T00:00:00Z",
|
||||||
|
url: "https://example.com/twtxt.txt",
|
||||||
|
};
|
||||||
|
const result = hashTwt(twt);
|
||||||
|
expect(result).toEqual("myzxbwxktuvs");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly provide v2 hash for twt after epoch", () => {
|
||||||
|
const twt = {
|
||||||
|
content: "Hello World!",
|
||||||
|
created: "2026-07-01T00:00:01Z",
|
||||||
|
url: "https://example.com/twtxt.txt",
|
||||||
|
};
|
||||||
|
const result = hashTwt(twt);
|
||||||
|
expect(result).toEqual("b3ropa74yjxx");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly provide v2 hash for twt after epoch", () => {
|
||||||
|
const twt = {
|
||||||
|
content: "Hello World!",
|
||||||
|
created: "2026-07-01T00:00:02Z",
|
||||||
|
url: "https://example.com/twtxt.txt",
|
||||||
|
};
|
||||||
|
const result = hashTwt(twt);
|
||||||
|
expect(result).toEqual("4ymrlnngtolc");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly provide v2 hash for twt after epoch", () => {
|
||||||
|
const twt = {
|
||||||
|
content: "Hello World!",
|
||||||
|
created: "2026-07-01T00:00:03Z",
|
||||||
|
url: "https://example.com/twtxt.txt",
|
||||||
|
};
|
||||||
|
const result = hashTwt(twt);
|
||||||
|
expect(result).toEqual("sdcslqv5epkb");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly provide v2 hash for twt after epoch", () => {
|
||||||
|
const twt = {
|
||||||
|
content: "Hello World!",
|
||||||
|
created: "2026-07-08T00:00:00Z",
|
||||||
|
url: "https://example.com/twtxt.txt",
|
||||||
|
};
|
||||||
|
const result = hashTwt(twt);
|
||||||
|
expect(result).toEqual("heim3egy5qik");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly provide v1 hash for twt before epoch when version is 0", () => {
|
||||||
|
const twt = {
|
||||||
|
content: "Hello World!",
|
||||||
|
created: "2026-06-30T23:59:58Z",
|
||||||
|
url: "https://example.com/twtxt.txt",
|
||||||
|
};
|
||||||
|
const result = hashTwt(twt, 0);
|
||||||
|
expect(result).toEqual("acwz4tq");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly provide v1 hash for twt before epoch when version is 0", () => {
|
||||||
|
const twt = {
|
||||||
|
content: "Hello World!",
|
||||||
|
created: "2026-06-30T23:59:57Z",
|
||||||
|
url: "https://example.com/twtxt.txt",
|
||||||
|
};
|
||||||
|
const result = hashTwt(twt, 0);
|
||||||
|
expect(result).toEqual("5fket4q");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly provide v2 hash for twt after epoch when version is 0", () => {
|
||||||
|
const twt = {
|
||||||
|
content: "Hello World!",
|
||||||
|
created: "2026-07-01T00:00:03Z",
|
||||||
|
url: "https://example.com/twtxt.txt",
|
||||||
|
};
|
||||||
|
const result = hashTwt(twt, 0);
|
||||||
|
expect(result).toEqual("sdcslqv5epkb");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly provide v2 hash for twt after epoch when version is 0", () => {
|
||||||
|
const twt = {
|
||||||
|
content: "Hello World!",
|
||||||
|
created: "2026-07-08T00:00:00Z",
|
||||||
|
url: "https://example.com/twtxt.txt",
|
||||||
|
};
|
||||||
|
const result = hashTwt(twt, 0);
|
||||||
|
expect(result).toEqual("heim3egy5qik");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly provide v1 hash for twt before epoch when version is 1", () => {
|
||||||
|
const twt = {
|
||||||
|
content: "Hello World!",
|
||||||
|
created: "2026-06-30T23:59:58Z",
|
||||||
|
url: "https://example.com/twtxt.txt",
|
||||||
|
};
|
||||||
|
const result = hashTwt(twt, 1);
|
||||||
|
expect(result).toEqual("acwz4tq");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly provide v2 hash for twt before epoch when version is 2", () => {
|
||||||
|
const twt = {
|
||||||
|
content: "Hello World!",
|
||||||
|
created: "2026-06-30T23:59:57Z",
|
||||||
|
url: "https://example.com/twtxt.txt",
|
||||||
|
};
|
||||||
|
const result = hashTwt(twt, 2);
|
||||||
|
expect(result).toEqual("pn6ju5op6nwn");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly provide v1 hash for twt after epoch when version is 1", () => {
|
||||||
|
const twt = {
|
||||||
|
content: "Hello World!",
|
||||||
|
created: "2026-07-01T00:00:03Z",
|
||||||
|
url: "https://example.com/twtxt.txt",
|
||||||
|
};
|
||||||
|
const result = hashTwt(twt, 1);
|
||||||
|
expect(result).toEqual("ufitwvq");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly provide v2 hash for twt after epoch when version is 2", () => {
|
||||||
|
const twt = {
|
||||||
|
content: "Hello World!",
|
||||||
|
created: "2026-07-08T00:00:00Z",
|
||||||
|
url: "https://example.com/twtxt.txt",
|
||||||
|
};
|
||||||
|
const result = hashTwt(twt);
|
||||||
|
expect(result).toEqual("heim3egy5qik");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1 @@
|
|||||||
import { dirname } from "node:path";
|
export const HASH_V2_EPOCH = `2026-07-01T00:00:00Z`;
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
|
|
||||||
export const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { Twt } from "./types.ts";
|
|||||||
import { blake2b } from "@exodus/blakejs";
|
import { blake2b } from "@exodus/blakejs";
|
||||||
|
|
||||||
import { base32Encode } from "./utils.ts";
|
import { base32Encode } from "./utils.ts";
|
||||||
|
import { HASH_V2_EPOCH } from "./constants.ts";
|
||||||
|
|
||||||
const dateRegex =
|
const dateRegex =
|
||||||
/^(\d{4})-(\d{2})-(\d{2})([tT ])(\d{2}):(\d{2}):(\d{2})\.?(\d{3})?(?:(?:([+-]\d{2}):?(\d{2}))|Z)?$/;
|
/^(\d{4})-(\d{2})-(\d{2})([tT ])(\d{2}):(\d{2}):(\d{2})\.?(\d{3})?(?:(?:([+-]\d{2}):?(\d{2}))|Z)?$/;
|
||||||
@@ -44,11 +45,19 @@ const formatRFC3339 = (date: string) => {
|
|||||||
].join("");
|
].join("");
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function hashTwt(twt: Twt): string {
|
export type HashableTwt = Pick<Twt, "content" | "created" | "url"> &
|
||||||
|
Partial<Omit<Twt, "content" | "created" | "url">>;
|
||||||
|
|
||||||
|
export default function hashTwt(twt: HashableTwt, version = 0): string {
|
||||||
const created = formatRFC3339(twt.created);
|
const created = formatRFC3339(twt.created);
|
||||||
const payload = [twt.url, created, twt.content].join("\n");
|
const payload = [twt.url, created, twt.content].join("\n");
|
||||||
|
|
||||||
return base32Encode(blake2b(payload, undefined, 32))
|
const encoded = base32Encode(blake2b(payload, undefined, 32)).toLowerCase();
|
||||||
.toLowerCase()
|
|
||||||
.slice(-7);
|
const createdDate = new Date(twt.created),
|
||||||
|
epochDate = new Date(HASH_V2_EPOCH);
|
||||||
|
|
||||||
|
if (version === 1 || (!version && createdDate < epochDate))
|
||||||
|
return encoded.slice(-7);
|
||||||
|
return encoded.slice(0, 12);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user