« get me outta code hell

html.js « util « src - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/util/html.js
blob: 338df71b893e5d01d571042b84919629037c98cf (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
/** @format */

// Some really simple functions for formatting HTML content.

// COMPREHENSIVE!
// https://html.spec.whatwg.org/multipage/syntax.html#void-elements
export const selfClosingTags = [
  'area',
  'base',
  'br',
  'col',
  'embed',
  'hr',
  'img',
  'input',
  'link',
  'meta',
  'source',
  'track',
  'wbr',
];

// Pass to tag() as an attributes key to make tag() return a 8lank string if the
// provided content is empty. Useful for when you'll only 8e showing an element
// according to the presence of content that would 8elong there.
export const onlyIfContent = Symbol();

// Pass to tag() as an attributes key to make children be joined together by the
// provided string. This is handy, for example, for joining lines by <br> tags,
// or putting some other divider between each child. Note this will only have an
// effect if the tag content is passed as an array of children and not a single
// string.
export const joinChildren = Symbol();

export function tag(tagName, ...args) {
  const selfClosing = selfClosingTags.includes(tagName);

  let openTag;
  let content;
  let attrs;

  if (typeof args[0] === 'object' && !Array.isArray(args[0])) {
    attrs = args[0];
    content = args[1];
  } else {
    content = args[0];
  }

  if (selfClosing && content) {
    throw new Error(`Tag <${tagName}> is self-closing but got content!`);
  }

  if (attrs?.[onlyIfContent] && !content) {
    return '';
  }

  if (attrs) {
    const attrString = attributes(args[0]);
    if (attrString) {
      openTag = `${tagName} ${attrString}`;
    }
  }

  if (!openTag) {
    openTag = tagName;
  }

  if (Array.isArray(content)) {
    const joiner = attrs?.[joinChildren];
    content = content.filter(Boolean).join(
      (joiner
        ? `\n${joiner}\n`
        : '\n'));
  }

  if (content) {
    if (content.includes('\n')) {
      return (
        `<${openTag}>\n` +
        content
          .split('\n')
          .map((line) => '    ' + line + '\n')
          .join('') +
        `</${tagName}>`
      );
    } else {
      return `<${openTag}>${content}</${tagName}>`;
    }
  } else {
    if (selfClosing) {
      return `<${openTag}>`;
    } else {
      return `<${openTag}></${tagName}>`;
    }
  }
}

export function escapeAttributeValue(value) {
  return value.replaceAll('"', '&quot;').replaceAll("'", '&apos;');
}

export function attributes(attribs) {
  return Object.entries(attribs)
    .map(([key, val]) => {
      if (typeof val === 'undefined' || val === null) return [key, val, false];
      else if (typeof val === 'string') return [key, val, true];
      else if (typeof val === 'boolean') return [key, val, val];
      else if (typeof val === 'number') return [key, val.toString(), true];
      else if (Array.isArray(val))
        return [key, val.filter(Boolean).join(' '), val.length > 0];
      else
        throw new Error(
          `Attribute value for ${key} should be primitive or array, got ${typeof val}`
        );
    })
    .filter(([_key, _val, keep]) => keep)
    .map(([key, val]) =>
      typeof val === 'boolean'
        ? `${key}`
        : `${key}="${escapeAttributeValue(val)}"`
    )
    .join(' ');
}