-
Notifications
You must be signed in to change notification settings - Fork 0
/
mod.ts
161 lines (136 loc) · 4.14 KB
/
mod.ts
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import { getStdin } from "https://deno.land/x/get_stdin@v1.1.0/mod.ts";
import { Arguments } from "https://deno.land/x/allo_arguments@v6.0.4/mod.ts";
import pretty from "npm:pretty@2.0.0";
interface URLType {
type: "link";
data: {
url: string;
label?: string;
};
}
interface TextType {
type: "text";
data: string;
}
type BlockInfo = URLType | TextType;
interface Args {
title: string | undefined;
help: boolean;
}
const defaultTitle = "Some hypertext";
try {
await main(getArguments());
} catch (error) {
Arguments.rethrowUnprintableException(error);
}
async function main(args: Args) {
if (args.help) return;
const value = await getStdin({ exitOnEnter: false });
const lines = value.split(`\n`);
const blocks: BlockInfo[] = [];
let isComment = false;
let isLink = false;
/**
* hypa definition:
* blocks are defined per-line.
* @ indicates a link, which might be followed by a line with ?
* ? indicates a label for the previous link line (otherwise it's considered text)
* # indicates a single line comment
* ### indicates the start or end of a multi-line comment block
* empty string lines (incl. whitespace-only lines) aren't rendered and delimit blocks
* anything else is rendered as a text block
*/
for (let i = 0; i < lines.length; i += 1) {
const line = lines[i].trim();
const firstChar = line.substring(0, 1);
if (isComment) { // waiting for ###
if (line.substring(0, 3) === "###") { // end comment
isComment = false;
}
continue; // no need to process further
}
if (isLink) { // waiting for ? or finish creating link
isLink = false; // link is completed
if (firstChar === "?") { // set label
const lastBlock = blocks[blocks.length - 1];
if (lastBlock.type === "link") {
lastBlock.data.label = line.substring(1).trim();
} else {
throw Error(`Somehow adding label for missing link at line: ${i}`);
}
continue; // no need to process further
}
}
if (firstChar === "@") { // is a link
isLink = true;
blocks.push({ type: "link", data: { url: line.substring(1).trim() } });
} else if (firstChar === "#") { // ignore content, and check if single or multiline comment
if (line.substring(0, 3) === "###") { // is multiline comment
isComment = true;
}
} else if (line) { // everything else is text, as long as it isn't an empty string
blocks.push({ type: "text", data: line });
}
}
const output = await buildHTML(args.title ?? defaultTitle, blocks);
Deno.stdout.write(new TextEncoder().encode(output));
}
function getArguments(): Args {
const info = [
"This program interprets a formatted text file as an 'hypa' formatted plaintext file.",
"It outputs this file as an HTML document.",
];
const args = new Arguments({
...Arguments.createHelpOptions(),
"title": {
shortName: "t",
description: "A header for the output document",
convertor: Arguments.stringConvertor,
},
})
.setDescription(info.join(`\n`));
if (args.isHelpRequested()) args.triggerHelp();
return args.getFlags();
}
async function buildHTML(header: string, blockData: BlockInfo[]) {
const css = await renderCSS();
const blocks = blockData.map((block) => {
switch (block.type) {
case "link":
return renderLink(block.data.url, block.data.label);
case "text":
return renderText(block.data);
}
}).join(``);
const doc = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${header}</title>
<style>${css}</style>
</head>
<body>
<main>${blocks}</main>
</body>
</html>
`;
return pretty(doc);
}
async function renderCSS() {
const module = await fetch(new URL("./render.css", import.meta.url));
const css = await module.text();
return css;
}
function renderLink(url: string, label?: string) {
return `
<div>
<a href="${url}">${label || url}</a>
</div>
`;
}
function renderText(text: string) {
return `<p>${text}</p>`;
}