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
|
import { Element, forEachChild, Node, Text } from "../dom.ts";
export function renderXml(root: Node): string {
return `<?xml version="1.0" encoding="utf-8"?>\n` + nodeToXmlText(root, {
indentLevel: 0,
});
}
type Context = {
indentLevel: number;
};
type Dtd = { type: "block" | "inline" };
function getDtd(name: string): Dtd {
switch (name) {
case "feed":
case "entry":
case "author":
return { type: "block" };
default:
return { type: "inline" };
}
}
function isInlineNode(n: Node): boolean {
if (n.kind === "text" || n.kind === "raw") {
return true;
}
return getDtd(n.name).type === "inline";
}
function isBlockNode(n: Node): boolean {
return !isInlineNode(n);
}
function nodeToXmlText(n: Node, ctx: Context): string {
if (n.kind === "text") {
return textNodeToXmlText(n);
} else if (n.kind === "raw") {
return n.html;
} else {
return elementNodeToXmlText(n, ctx);
}
}
function textNodeToXmlText(t: Text): string {
const s = encodeSpecialCharacters(t.content);
// TODO: 日本語で改行するときはスペースを入れない
return s.replaceAll(/\n */g, " ");
}
function encodeSpecialCharacters(s: string): string {
return s.replaceAll(/&(?!\w+;)/g, "&")
.replaceAll(/</g, "<")
.replaceAll(/>/g, ">")
.replaceAll(/'/g, "'")
.replaceAll(/"/g, """);
}
function elementNodeToXmlText(e: Element, ctx: Context): string {
let s = "";
s += indent(ctx);
s += `<${e.name}`;
const attributes = getElementAttributes(e);
if (attributes.length > 0) {
s += " ";
for (let i = 0; i < attributes.length; i++) {
const [name, value] = attributes[i];
s += `${name}="${encodeSpecialCharacters(value)}"`;
if (i !== attributes.length - 1) {
s += " ";
}
}
}
s += ">";
if (isBlockNode(e)) {
s += "\n";
}
ctx.indentLevel += 1;
forEachChild(e, (c) => {
s += nodeToXmlText(c, ctx);
});
ctx.indentLevel -= 1;
if (isBlockNode(e)) {
s += indent(ctx);
}
s += `</${e.name}>`;
s += "\n";
return s;
}
function indent(ctx: Context): string {
return " ".repeat(ctx.indentLevel);
}
function getElementAttributes(e: Element): [string, string][] {
return [...Object.entries(e.attributes)]
.filter((a) => !a[0].startsWith("__"))
.sort(
(a, b) => {
// Special rules:
if (e.name === "link") {
if (a[0] === "href" && b[0] === "rel") {
return 1;
}
if (a[0] === "rel" && b[0] === "href") {
return -1;
}
if (a[0] === "href" && b[0] === "type") {
return 1;
}
if (a[0] === "type" && b[0] === "href") {
return -1;
}
}
// General rules:
if (a[0] > b[0]) return 1;
else if (a[0] < b[0]) return -1;
else return 0;
},
);
}
|