summaryrefslogtreecommitdiffhomepage
path: root/services/blog/nuldoc-src/renderers/xml.ts
blob: 523567abdb7b656739d5d064e8c087c9f448010e (plain)
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, "&amp;")
    .replaceAll(/</g, "&lt;")
    .replaceAll(/>/g, "&gt;")
    .replaceAll(/'/g, "&apos;")
    .replaceAll(/"/g, "&quot;");
}

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;
      },
    );
}