« 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: bdb385b5539a474eff5ab9316ffa9f5eca81b23f (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
129
130
131
132
133
134
135
136
137
138
139
/** @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(attrs);
    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(' ');
}

// Ensures the passed value is an array of elements, for usage in [...spread]
// syntax. This may be used when it's not guaranteed whether the return value of
// an external function is one child or an array, or in combination with
// conditionals, e.g. fragment(cond && [x, y, z]).
export function fragment(childOrChildren) {
  if (!childOrChildren) {
    return [];
  }

  if (Array.isArray(childOrChildren)) {
    return childOrChildren;
  }

  return [childOrChildren];
}