Change from pnpm to npm, add ./link.sh shortcut for npm style package linking
[jstize.git] / jstize.js
1 let CleanCSS = require('@ndcode/clean-css')
2 let assert = require('assert')
3 let html_entities = require('html-entities')
4 let html_minifier = require('@ndcode/html-minifier')
5 let node_stringify = require('node-stringify')
6 let uglify_es = require('uglify-es')
7
8 let clean_css = new CleanCSS({format: 'beautify'})
9 let xml_entities = new html_entities.XmlEntities()
10
11 let jstize = (text, options) => {
12   options = Object.assign(
13     {indent: 2, initial_indent: 0},
14     options || {}
15   )
16   let tags = []
17   let indent = options.indent * options.initial_indent
18   let buffer = []
19   let parse = text => {
20     new html_minifier.HTMLParser(
21       text,
22       {
23         html5: true,
24         start: (tag, attrs, unary) => { //, unarySlash, autoGenerated) {
25           //let out = `<${tag}`
26           //for (let i = 0; i < attrs.length; ++i) {
27           //  out += ` ${attrs[i].name}`
28           //  if (attrs[i].value !== undefined)
29           //    out += `="${attrs[i].value}"`
30           //}
31           //out += '>'
32           let out = tag
33           let out1 = ''
34           let prefix = '('
35           let suffix = ''
36           for (let i = 0; i < attrs.length; ++i) {
37             if (attrs[i].name == 'class') {
38               let fields = xml_entities.decode(attrs[i].value).split(' ')
39               for (let j = 0; j < fields.length; ++j)
40                 out = out.replace(
41                   /([.#])([^.#]*-[0-9]*)$/,
42                   (match, sep, name) => sep + node_stringify(name)
43                 ) + '.' + fields[j]
44             }
45             else if (attrs[i].name == 'id') {
46               let fields = xml_entities.decode(attrs[i].value).split(' ')
47               for (let j = 0; j < fields.length; ++j)
48                 out += '#' + fields[j]
49             }
50             else {
51               out1 += prefix + attrs[i].name
52               if (attrs[i].value !== undefined)
53                 out1 +=
54                   '=' + JSON.stringify(xml_entities.decode(attrs[i].value))
55               prefix = ' '
56               suffix = ')'
57             }
58           }
59           buffer.push(
60             ' '.repeat(indent) +
61             out.replace(
62               /([.#])(([^.#]*-)?style)$/,
63               (match, sep, name) => sep + node_stringify(name)
64             ) +
65             out1 +
66             suffix +
67             (unary ? ' {}\n' : ' {\n')
68           )
69           if (!unary) {
70             tags.push(tag)
71             indent += options.indent
72           }
73         },
74         end: tag => { //, attrs, autoGenerated) => {
75           assert(tag === tags.pop())
76           indent -= options.indent
77           //buffer.push(`</${tag}>`)
78           buffer.push(
79             (
80               buffer.length && buffer[buffer.length - 1].slice(-2) == '{\n' ?
81                 buffer.pop().slice(0, -1) :
82                 ' '.repeat(indent)
83             ) +
84             '}\n'
85           )
86         },
87         chars: text => { //, prevTag, nextTag) {
88           let out
89           if (tags.length && tags[tags.length - 1] === 'script') {
90             let render = uglify_es.minify(
91               text,
92               {
93                 compress: false,
94                 mangle: false,
95                 output: {
96                   beautify: true,
97                   indent_level: options.indent,
98                   shebang: false
99                 },
100                 toplevel: true
101               }
102             )
103             if (render.error !== undefined)
104               throw render.error
105             //buffer.push(render.code)
106             out = render.code.split('\n')
107             if (out.length && out[out.length - 1].length === 0)
108               out.pop()
109           }
110           else if (tags.length && tags[tags.length - 1] === 'style') {
111             let render = clean_css.minify(text)
112             //for (let i = 0; i < render.warnings.length; ++i)
113             //  console.log(`clean-css warning: ${render.warnings[i]}`)
114             //buffer.push(render.styles)
115             out = render.styles.split('\n')
116             if (out.length && out[out.length - 1].length === 0)
117               out.pop()
118           }
119           else
120             //buffer.push(text)
121             out = [node_stringify(xml_entities.decode(text))]
122           for (let i = 0; i < out.length; ++i)
123             buffer.push(' '.repeat(indent) + out[i] + '\n')
124         },
125         comment: (text, nonStandard) => {
126           let prefix = nonStandard ? '<!' : '<!--'
127           let suffix = nonStandard ? '>' : '-->'
128           let match = text.match(/^(\[if\s[^\]]+]>)([\s\S]*?)(<!\[endif])$/)
129           if (match) {
130             buffer.push(
131               `${' '.repeat(indent)}_out.push('${prefix}${match[1]}')\n`
132             )
133             parse(match[2])
134             buffer.push(
135               `${' '.repeat(indent)}_out.push('${match[3]}${suffix}')\n`
136             )
137           }
138           else
139             buffer.push(
140               `${' '.repeat(indent)}_out.push('${prefix}${text}${suffix}')\n`
141             )
142         },
143         doctype: doctype => {
144           //buffer.push(doctype)
145           buffer.push(
146             `${' '.repeat(indent)}_out.push('${doctype}')\n`
147           )
148         }
149       }
150     )
151   }
152   parse(html_minifier.minify(text, {collapseWhitespace: true}))
153   assert(tags.length === 0)
154   return buffer.join('')
155 }
156
157 module.exports = jstize