Newest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { cors } from "npm:hono/cors";
import { Hono } from "npm:hono@3";
import Typesense from "npm:typesense";
import type { SearchParams, SearchParamsWithPreset, SearchResponseHit } from "npm:typesense/lib/Typesense/Documents";
const MAX_SEARCH_RADIUS_IN_MILES = 10;
const COLLECTIONS = {
"resources": "resources",
};
const typesenseClient = new Typesense.Client({
"nodes": [{
"host": Deno.env.get("TYPESENSE_HOST") || "", // For Typesense Cloud use xxx.a1.typesense.net
"port": 443, // For Typesense Cloud use 443
"protocol": "https", // For Typesense Cloud use https
}],
"apiKey": Deno.env.get("TYPESENSE_API_KEY") || "",
"connectionTimeoutSeconds": 10,
"retryIntervalSeconds": 0.1,
"healthcheckIntervalSeconds": 2,
"logLevel": "debug",
});
interface Resource {
id: string;
name: string;
address: {
full_address: string;
latitude: number;
longitude: number;
};
website: string;
phone: string;
user_email: string;
description: string;
utc_offset: number;
featured_image_url: string;
hours_text: string | null;
hours: {
day: string;
day_int: number;
open_time: string | null;
close_time: string | null;
}[];
services: {
id: string | number;
name: string;
description: string;
category: {
icon: string;
name: ResourceTypes;
};
}[];
qualifications: {
id: string | number;
name: string;
}[];
// Custom field added by Cameron Pak to calculate on the fly.
distance_between: number;
}
const CATEGORY_IDS_FROM_TYPESENSE = {
// Favorites does not exist in Typesense but is a ResourceTypes type.
"Favorites": -1,
"Food": 53,
"Shelter": 52,
"Churches": 56,
"Resources": 57,
"Medical": 54,
"Addiction Recovery": 58,
"Domestic Violence": 51,
"Housing": 69,
};
type ResourceTypes = keyof typeof CATEGORY_IDS_FROM_TYPESENSE;
/**
* Converts meters to miles.
*
* @param {number} meters - the distance in meters to convert
* @return {number} the distance in miles
*/
function convertMetersToMiles(meters: number): number {
return 0.000621371 * meters;
}
// Round a number to the nearest quarter.
function roundToQuarter(number: number) {
let remainder = number % 1;
// I only want 1/8th of a mile to appear when when that's the only remainder.
if (number < 1 && remainder <= 0.125) {
return Math.floor(number) + 0.125;
} else if (remainder <= 0.375) {
return Math.floor(number) + 0.25;
} else if (remainder <= 0.625) {
return Math.floor(number) + 0.50;
} else if (remainder <= 0.875) {
return Math.floor(number) + 0.75;
} else {
return Math.ceil(number);

Fancy animated SVGs in readmes, along with centering and image sizing.

<div align="center"><img width=200 src="https://gpanders.com/img/DEC_VT100_terminal.jpg"></div>
<p align="center">
<img src="https://maxm-animatedreadmesvg.web.val.run/comet.svg" />
</p>

<p align="center">
<img src="https://maxm-animatedreadmesvg.web.val.run/custom text!" />
</p>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/** @jsxImportSource https://esm.sh/react */
import { renderToString } from "npm:react-dom/server";
const genSVG = (request: Request): string => {
let pathname = new URL(request.url).pathname;
if (pathname === "/comet.svg") {
return renderToString(
<svg version="1.1" baseProfile="full" width="160" height="160" xmlns="http://www.w3.org/2000/svg">
<circle cx="80" cy="80" r="50" fill="#1b6ca8" />
<g transform=" matrix(0.866, -0.5, 0.25, 0.433, 80, 80)">
<path d="M 0,70 A 65,70 0 0,0 65,0 5,5 0 0,1 75,0 75,70 0 0,1 0,70Z" fill="#0a97b0">
<animateTransform
attributeName="transform"
type="rotate"
from="360 0 0"
to="0 0 0"
dur="2s"
repeatCount="indefinite"
/>
</path>
</g>
<path d="M 50,0 A 50,50 0 0,0 -50,0Z" transform="matrix(0.866, -0.5, 0.5, 0.866, 80, 80)" fill="#1b6ca8" />
</svg>,
);
}
pathname === "/" && (pathname = "/hello");
// taken from https://codepen.io/dustindwayne/pen/zYXZzMd
return renderToString(
<svg width="600" height="150" xmlns="http://www.w3.org/2000/svg">
<style>
{`
html,
body {
background-color: #121212;
display: flex;
justify-content: center;
align-items: center;
margin: 0;
padding: 0;
}
text {
font-family: "sans-serif";
text-align: center;
stroke-width: 4px;
stroke-dasharray: 80;
fill: rgba(0,0,0,0);
stroke-linecap: round;
animation: follow 1.8s linear infinite;
transition: all 2.1s;
}
text:hover {
stroke-dasharray: 1000;
stroke-dashoffset: 0;
stroke-width: 2px;
}
p {
color: #fff;
position: absolute;
bottom: 25px;
font-size: 18px;
font-family: "Poppins";
}
@keyframes follow {
0% {
stroke-dashoffset: 0;
stroke: #ff69b4;
filter: drop-shadow(0px 0px 5px #ff69b4);
}
50% {
stroke: #87ceeb;
filter: drop-shadow(0px 0px 5px #87ceeb);
}
100% {
stroke-dashoffset: 160;
stroke: #ff69b4;
filter: drop-shadow(0px 0px 5px #ff69b4);
}
}
`}
</style>
<text x="40" y="100" font-size="100">{decodeURI(pathname.slice(1, pathname.length))}</text>
</svg>,
);
};
export const reactExample = (request: Request) =>
new Response(
genSVG(request),
{
headers: {
"Content-Type": "image/svg+xml",
},
},
);
1
2
3
4
5
6
7
8
import { email } from "https://esm.town/v/std/email";
console.log(
await email({
subject: "Test mail from val.town!",
text: "hello from val.town",
}),
);

Static Chess

chess

Check it out here: https://chess.maxmcd.com

Plain, brutalist, no bloat chess. Every page is only html and css. Every chess move is made by clicking a link. Send a link to your friend and they'll send you one back to make your move. No silly animations or slick interactivity to trip up your gameplay. When Google indexes this site will we successfully compute all possible chess moves?

Functionality is quite limited, and things might be broken. Please let me know if you find bugs!

Inspired by this HN discussion about sites that have all possible game states of tic-tac-toe.

I plan on extending this to support real gameplay. I think it could be a nice simple interface for long form games with friends. Might also be fun to add a static AI to play against. Feel free to PR any changes if you'd like to see something added.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/** @jsxImportSource https://esm.sh/react */
import { Chess, Move, Square } from "npm:chess.js";
import minify from "npm:css-simple-minifier";
import { renderToString } from "npm:react-dom/server";
class StaticChess {
size = 8;
rows = Array.from({ length: this.size }, (_, i) => i);
squares = Array.from({ length: this.size }, (_, i) => i);
constructor() {}
async fetch(req: Request): Promise<Response> {
if (new URL(req.url).pathname === "/robots.txt") {
return new Response("User-agent: *\nDisallow: /");
}
const gameInfo = parseURL(req.url);
if (gameInfo === undefined) {
return new Response("Not Found", { status: 404 });
}
const game = new Game(gameInfo.game, gameInfo.selected);
return new Response(
renderToString(
<html>
<head>
<title>Static Chess</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="https://fav.farm/♟️" />
<style>{minify(CSS)}</style>
</head>
<body>
<div id="code-on-vt-host">
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/github-fork-ribbon-css/0.2.3/gh-fork-ribbon.min.css"
/>
<a
href="https://www.val.town/v/maxm/staticChess"
rel="source"
target="_blank"
className="github-fork-ribbon"
data-ribbon="Code on Val Town"
title="Code on Val Town"
>
Code on Val Town
</a>
</div>
<h1>Static Chess</h1>
<div>
<a href="https://www.val.town/v/maxm/staticChess">info</a> - <a href="/">reset</a>
</div>
<div className="board">
{this.rows.map(row => (
<div key={row} className="row">{this.squares.map(square => game.squareContent(row, square))}</div>
))}
</div>
<div className="info">
{game.selected
? "Click a highted square to move the selected piece, or select a different piece."
: `It is ${{ w: "white", b: "black" }[game.game.turn()]}'s turn. Click a piece to make a move.`}
</div>
</body>
</html>,
),
{ headers: { "content-type": "text/html; charset=utf-8;" } },
);
}
}
class Game {
game: Chess;
selected?: string;
selectable: string[];
board;
nextMoves: { [key: string]: Move };
fen: string;
constructor(game: Chess, selected?: string) {
this.game = game;
this.selected = selected;
this.board = game.board();
this.fen = game.fen().replaceAll(" ", "_");
this.nextMoves = {};
this.selectable = game.moves({ verbose: true }).map((m) => m.from.toString());
if (this.selected) {
var moves = game.moves({
square: selected as Square,
verbose: true,
});
for (const move of moves) {
this.nextMoves[move.to] = move;
}
}
}
squareContent(row: number, square: number) {
const pos = indexToPos(row, square);
const color = this.board[row][square]?.color;
let className = "square";
1
2
3
4
5
6
// let's ask openai's new gpt-4o model to tell us a joke
import { chat } from "https://esm.town/v/stevekrouse/openai";
const { content } = await chat("Tell me a joke", { max_tokens: 50, model: "gpt-4o" });
console.log(content);

Decorator Router

Fair simple decorator based router, with GET/POST and middleware support.

demo

live demo: https://yieldray-decorator_router_demo.web.val.run/

Create valimport { get, post, all, use, registered, handler, type Context } from "https://esm.town/v/yieldray/decorator_router"; import { parseBearerAuth, transformResponse } from "https://esm.sh/serve-router@1.1.0/utils"; interface User { id: number; name: string; } const users: User[] = [ { id: 0, name: "Alice" }, { id: 1, name: "Ray" }, ]; class _Server { /** * Decorator @get: Parses URLSearchParams into an object as the first parameter. */ @get("/users") getAllUsers() { return users; // Automatically wrapped in a Response.json } @get("/getUserByName") // GET /getUserByName?name=Alice getUserByName({ name }: Record<string, string>) { const user = users.find((u) => u.name === name); if (user) { return user; // Automatically wrapped as Response.json(user) } // Optionally, manually return a Response object return Response.json({ error: "not found" }, { status: 404 }); } @get("/user/:id") // GET /user/123 user(_: unknown, { params: { id } }: Context) { return users.find((u) => u.id === Number(id)); } /** * Decorator @post: Parses the request body into an object as the first parameter. */ @post("/user") // POST /user async createUser(user: User) { if (users.find((u) => u.id === user.id)) { return { error: "already exists!" }; } await users.push(user); // Assume insertion into a database return { ok: true, users }; } @post("/user/:id") // POST /user/123 async updateUser(user: User, { params: { id }, request }: Context) { const token = parseBearerAuth(request.headers.get("Authorization")!); // Additional logic here... } @all("/") home({ request }: { request: Request }) { return { registered, method: request.method, url: request.url, headers: Object.fromEntries(request.headers.entries()), }; } @use("/*") async corsMiddleware({ next, request }: Context) { const resp = await next(); return transformResponse(resp, { headers: { "Access-Control-Allow-Origin": request.headers.get("origin") || "*", }, }); } } // For Deno: Deno.serve(handler); // For val.town: export default handler;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import ServeRouter from "https://esm.sh/serve-router@1.1.0";
import { parseRequestBody } from "https://esm.sh/serve-router@1.1.0/utils";
export const app = ServeRouter({
onError(error) {
console.log(error);
},
});
const _registered: Array<{ method: string; path: string }> = [];
export const registered: Readonly<typeof _registered> = _registered;
export const handler = (req: Request) => app.fetch(req);
export interface Context {
request: Request;
params: Record<string, string | undefined>;
next: () => Promise<Response>;
}
export function get(path: string) {
return (
target: (query: Record<string, string>, ctx: Context) => unknown,
_context: ClassMethodDecoratorContext,
) => {
_registered.push({ method: "GET", path });
app.get(path, async (request: Request, { params, next }) => {
const resp = await target(Object.fromEntries(new URL(request.url).searchParams.entries()), {
request,
params,
next,
});
if (resp instanceof Response) return resp;
if (resp == undefined) return next();
return Response.json(resp);
});
};
}
export function post(path: string) {
return (target: (body: any, ctx: Context) => unknown, _context: ClassMethodDecoratorContext) => {
_registered.push({ method: "POST", path });
app.post(path, async (request: Request, { params, next }) => {
const resp = await target(await parseRequestBody(request, "application/json"), { request, params, next });
if (resp instanceof Response) return resp;
if (resp == undefined) return next();
return Response.json(resp);
});
};
}
export function all(path: string) {
return (target: (ctx: Context) => unknown, _context: ClassMethodDecoratorContext) => {
_registered.push({ method: "*", path });
app.all(path, async (request: Request, { params, next }) => {
const resp = await target({ request, params, next });
if (resp instanceof Response) return resp;
if (resp == undefined) return next();
return Response.json(resp);
});
};
}
export function use(path: string) {
return (target: (ctx: Context) => unknown, _context: ClassMethodDecoratorContext) => {
app.use(path, async (request: Request, { params, next }) => {
const resp = await target({ request, params, next });
if (resp instanceof Response) return resp;
if (resp == undefined) return next();
return Response.json(resp);
});
};
}

OpenAI ChatGPT helper function

This val uses your OpenAI token if you have one, and the @std/openai if not, so it provides limited OpenAI usage for free.

Create valimport { chat } from "https://esm.town/v/stevekrouse/openai"; const { content } = await chat("Hello, GPT!"); console.log(content);
Create valimport { chat } from "https://esm.town/v/stevekrouse/openai"; const { content } = await chat( [ { role: "system", content: "You are Alan Kay" }, { role: "user", content: "What is the real computer revolution?"} ], { max_tokens: 50, model: "gpt-4" } ); console.log(content);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import type { ChatCompletion, ChatCompletionCreateParamsNonStreaming, Message } from "npm:@types/openai";
async function getOpenAI() {
// if you don't have a key, use our std library version
if (Deno.env.get("OPENAI_API_KEY") === undefined) {
const { OpenAI } = await import("https://esm.town/v/std/openai");
return new OpenAI();
} else {
const { OpenAI } = await import("npm:openai");
return new OpenAI();
}
}
/**
* Initiates a chat conversation with OpenAI's GPT model and retrieves the content of the first response.
* This function can handle both single string inputs and arrays of message objects.
* It supports various GPT models, allowing for flexibility in choosing the model based on the application's needs.
*
* @param {string | Message[]} input - The input message(s) to send to GPT. Can be a single string or an array of message objects.
* @param {object} options - Additional options for the completion request.
* @returns {Promise<string>} A promise that resolves to the content of the first response from the completion.
*/
export async function chat(
input: string | Message[],
options?: Omit<ChatCompletionCreateParamsNonStreaming, "messages">,
): Promise<ChatCompletion & { content: string }> {
const openai = await getOpenAI();
const messages = Array.isArray(input) ? input : [{ role: "user", content: input }];
const createParams: ChatCompletionCreateParamsNonStreaming = {
max_tokens: 30,
model: "gpt-3.5-turbo",
...(options ?? {}),
messages,
};
const completion = await openai.chat.completions.create(createParams);
return { ...completion, content: completion.choices[0].message.content };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
type FrequentieTable = Record<string, number>;
type IntermediaryNode = [string, number, Node[]];
type LeafNode = [string, number];
type Node = IntermediaryNode | LeafNode;
type HuffmanTree = [IntermediaryNode];
type Encoding = number[];
type EncodedData = number[];
type EncodingTable = Record<string, Encoding>;
export class BinaryReader {
protected chunk: number;
protected pos: number;
protected bitPos: number;
protected bitCount: number;
view: DataView;
constructor(arrayBuffer: ArrayBuffer) {
this.view = new DataView(arrayBuffer);
this.pos = 0;
this.bitPos = -1;
this.chunk = 0;
this.bitCount = 0;
}
protected getData(type = "Uint8") {
if (this.view.byteLength > this.pos) {
this.bitPos = -1;
// @ts-ignore
return this.view[`get${type}`](this.pos++);
}
throw new RangeError();
}
get buffer() {
return this.view.buffer;
}
getBytePosition() {
return this.pos;
}
seek(pos: number) {
const oldPos = this.pos;
this.pos = pos;
return oldPos;
}
peak(index = this.pos + 1) {
if (this.view.byteLength > index && index > -1) {
return this.view.getUint8(index);
}
return null;
}
peakBit() {
const chunk = this.chunk;
const pos = this.pos;
const bitPos = this.bitPos;
const bitCount = this.bitCount;
const bit = this.getBit();
this.bitPos = bitPos;
this.chunk = chunk;
this.pos = pos;
this.bitCount = bitCount;
return bit;
}
getPadSize() {
if (this.chunk === null) {
return 0;
} else {
const bitCount = getBitCount(this.chunk);
return 8 - bitCount;
}
}
getBitPos() {
return getBitCount(this.chunk) - 1 + this.getPadSize();
}
getBit() {
if (this.bitPos === -1) {
this.chunk = this.getData();
this.bitPos = this.getBitPos();
this.bitCount = getBitCount(this.chunk);
}
if (this.chunk === null) return null;
const bitCount = this.bitCount;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/** @jsxImportSource https://esm.sh/react */
import { Chess, Move, Square } from "npm:chess.js";
import minify from "npm:css-simple-minifier";
import { renderToString } from "npm:react-dom/server";
class StaticChess {
size = 8;
rows = Array.from({ length: this.size }, (_, i) => i);
squares = Array.from({ length: this.size }, (_, i) => i);
constructor() {}
async fetch(req: Request): Promise<Response> {
if (new URL(req.url).pathname === "/robots.txt") {
return new Response("User-agent: *\nDisallow: /");
}
const gameInfo = parseURL(req.url);
if (gameInfo === undefined) {
return new Response("Not Found", { status: 404 });
}
const game = new Game(gameInfo.game, gameInfo.selected);
return new Response(
renderToString(
<html>
<head>
<title>Static Chess</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="https://fav.farm/♟️" />
<style>{minify(CSS)}</style>
</head>
<body>
<div id="code-on-vt-host">
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/github-fork-ribbon-css/0.2.3/gh-fork-ribbon.min.css"
/>
<a
href="https://www.val.town/v/maxm/staticChess"
rel="source"
target="_blank"
className="github-fork-ribbon"
data-ribbon="Code on Val Town"
title="Code on Val Town"
>
Code on Val Town
</a>
</div>
<h1>Static Chess</h1>
<div>
<a href="https://www.val.town/v/maxm/staticChess">info</a> - <a href="/">reset</a>
</div>
<div className="board">
{this.rows.map(row => (
<div key={row} className="row">{this.squares.map(square => game.squareContent(row, square))}</div>
))}
</div>
<div className="info">
{game.selected
? "Click a highted square to move the selected piece, or select a different piece."
: `It is ${{ w: "white", b: "black" }[game.game.turn()]}'s turn. Click a piece to make a move.`}
</div>
</body>
</html>,
),
{ headers: { "content-type": "text/html; charset=utf-8;" } },
);
}
}
class Game {
game: Chess;
selected?: string;
selectable: string[];
board;
nextMoves: { [key: string]: Move };
fen: string;
constructor(game: Chess, selected?: string) {
this.game = game;
this.selected = selected;
this.board = game.board();
this.fen = game.fen().replaceAll(" ", "_");
this.nextMoves = {};
this.selectable = game.moves({ verbose: true }).map((m) => m.from.toString());
if (this.selected) {
var moves = game.moves({
square: selected as Square,
verbose: true,
});
for (const move of moves) {
this.nextMoves[move.to] = move;
}
}
}
squareContent(row: number, square: number) {
const pos = indexToPos(row, square);
const color = this.board[row][square]?.color;
let className = "square";

markdown.download

Handy microservice/library to convert various data sources into markdown. Intended to make it easier to consume the web in ereaders

Introductory blog post: https://taras.glek.net/post/markdown.download/

Package: https://jsr.io/@tarasglek/markdown-download

Features

  • Apply readability
  • Further convert article into markdown to simplify it
  • Allow webpages to be viewable as markdown via curl
  • Serve markdown converted to html to browsers
  • Extract youtube subtitles

Source

https://github.com/tarasglek/markdown-download

https://www.val.town/v/taras/markdown_download

License: MIT

Usage: https://markdown.download/ + URL

Dev: https://val.markdown.download/ + URL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { isProbablyReaderable, Readability } from "npm:@mozilla/readability@^0.5.0";
import { DOMParser } from "npm:linkedom@0.16.10";
import { marked } from "npm:marked@12.0.1";
import { getSubtitles } from "npm:youtube-captions-scraper@^2.0.1";
import { YouTube } from "npm:youtube-sr@4.3.11";
const isCloudflareWorker = typeof Request !== "undefined" && typeof Response !== "undefined";
// init async loading of modules
const AgentMarkdownImport = isCloudflareWorker ? import("npm:agentmarkdown@6.0.0") : null;
const TurndownService = isCloudflareWorker ? null : await import("npm:turndown@^7.1.3");
/**
* converts HTML to markdown
* @returns markdown in string
*/
export async function html2markdown(html: string): Promise<string> {
if (AgentMarkdownImport) {
// TurndownService doesn't work on cf
// Dynamically import AgentMarkdown when running in Cloudflare Worker
const { AgentMarkdown } = await AgentMarkdownImport;
return await AgentMarkdown.produce(html);
} else {
// Dynamically import TurndownService otherwise
return new (await TurndownService)().turndown(html);
}
}
/**
* extracts article from html
* then converts it to md
* @returns markdown in string
*/
export async function readability2markdown(html: string): Promise<{ title: string; markdown: string }> {
const doc = await (new DOMParser().parseFromString(html, "text/html"));
const reader = new Readability(doc);
const article = reader.parse();
const markdown = await html2markdown(article?.content || "");
return { title: doc.title.textContent, markdown };
}
function getYoutubeVideoID(url: URL): string | null {
const regExp = /(?:youtube\.com\/(?:[^/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?/\s]{11})/i;
const match = url.href.match(regExp);
return match ? match[1] : null;
}
function response(message: string, contentType = "text/markdown"): Response {
const headers = new Headers();
headers.set("Access-Control-Allow-Origin", "*");
headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
headers.set("Access-Control-Max-Age", "86400");
headers.set("Content-Type", contentType);
return new Response(message, {
status: 200,
headers: headers,
});
}
function err(msg: string): Response {
const errorMessage = JSON.stringify({
error: {
message: msg,
code: 400,
},
});
return response(errorMessage, "application/json");
}
function fudgeURL(url: string) {
try {
return new URL(url);
} catch (e) {
// console.log("Url parsing failed", e.stack);
return new URL("https://" + url);
}
}
function processInput(req: Request) {
let ret = {
url: undefined as undefined | URL,
response: undefined as undefined | Response,
};
const myurl = new URL(req.url);
let pathname = myurl.pathname.substring(1) + myurl.search;
if (!pathname.startsWith("http")) {
const urlAsFormParam = myurl.searchParams.get("url");
if (urlAsFormParam) {
pathname = urlAsFormParam;
} else if (pathname.length < 2) {
ret.response = response(
generate_ui(
"URL to convert to markdown:",
"https://www.val.town/v/curtcox/markdown_download",
"markdown.download",
),
"text/html",