Browse Source

Compile to HTML

master
Dylan Baker 3 years ago
parent
commit
5d73f82733
5 changed files with 269 additions and 1 deletions
  1. 8
    0
      src/ast.js
  2. 19
    0
      src/compiler.js
  3. 160
    1
      src/core.js
  4. 18
    0
      test/compilerTest.js
  5. 64
    0
      test/coreTest.js

+ 8
- 0
src/ast.js View File

@@ -1,3 +1,5 @@
1
+const selfClosingTags = require('./util/selfClosingTags')
2
+
1 3
 class Node {
2 4
   constructor(opts = false) {
3 5
     if (opts) {
@@ -14,6 +16,12 @@ module.exports = {
14 16
   Boolean: class Boolean extends Node {},
15 17
   Conditional: class Conditional extends Node {},
16 18
   Definition: class Definition extends Node {},
19
+  HTMLElement: class HTMLElement extends Node {
20
+    constructor(opts) {
21
+      super(opts)
22
+      this.selfClosing = selfClosingTags.includes(opts.name)
23
+    }
24
+  },
17 25
   Identifier: class Identifier extends Node {},
18 26
   Lambda: class Lambda extends Node {},
19 27
   LetBinding: class LetBinding extends Node {},

+ 19
- 0
src/compiler.js View File

@@ -18,6 +18,25 @@ module.exports = class Compiler {
18 18
         return node.value
19 19
       case AST.Lambda:
20 20
         return '<lambda>'
21
+      case AST.HTMLElement:
22
+        let el = `<${node.name}`
23
+
24
+        if (node.attributes.length > 0) {
25
+          el += ' '
26
+          el += node.attributes.map(attr => `${attr.name}="${this.compileNode(attr.value)}"`).join(' ')
27
+        }
28
+
29
+        el += '>'
30
+
31
+        if (node.contents.length > 0) {
32
+          el += node.contents.map(content => this.compileNode(content)).join('')
33
+        }
34
+
35
+        if (node.selfClosing === false) {
36
+          el += `</${node.name}>`
37
+        }
38
+
39
+        return el
21 40
     }
22 41
   }
23 42
 }

+ 160
- 1
src/core.js View File

@@ -1,7 +1,15 @@
1 1
 const AST = require('./ast')
2 2
 const OsloError = require('./osloError')
3 3
 
4
-module.exports = {
4
+function element(name, args) {
5
+  return new AST.HTMLElement({
6
+    name: name,
7
+    attributes: args.filter(arg => arg.constructor === AST.Attribute),
8
+    contents: args.filter(arg => arg.constructor !== AST.Attribute),
9
+  })
10
+}
11
+
12
+const core = {
5 13
   '+': (a, b) => new AST.Number({ value: a.value + b.value }),
6 14
   '-': (a, b) => new AST.Number({ value: a.value - b.value }),
7 15
   '*': (a, b) => new AST.Number({ value: a.value * b.value }),
@@ -17,3 +25,154 @@ module.exports = {
17 25
   first: xs => xs.elements[0],
18 26
   rest: xs => new AST.List({ elements: xs.elements.slice(1) }),
19 27
 }
28
+
29
+const tags = [
30
+  '!DOCTYPE',
31
+  '!doctype',
32
+  'a',
33
+  'abbr',
34
+  'acronym',
35
+  'address',
36
+  'applet',
37
+  'area',
38
+  'article',
39
+  'aside',
40
+  'audio',
41
+  'b',
42
+  'base',
43
+  'basefont',
44
+  'bdi',
45
+  'bdo',
46
+  'bgsound',
47
+  'big',
48
+  'blink',
49
+  'blockquote',
50
+  'body',
51
+  'br',
52
+  'button',
53
+  'canvas',
54
+  'caption',
55
+  'center',
56
+  'cite',
57
+  'code',
58
+  'col',
59
+  'colgroup',
60
+  'command',
61
+  'content',
62
+  'data',
63
+  'datalist',
64
+  'dd',
65
+  'del',
66
+  'details',
67
+  'dfn',
68
+  'dialog',
69
+  'dir',
70
+  'div',
71
+  'dl',
72
+  'dt',
73
+  'element',
74
+  'em',
75
+  'embed',
76
+  'fieldset',
77
+  'figcaption',
78
+  'figure',
79
+  'font',
80
+  'footer',
81
+  'form',
82
+  'frame',
83
+  'frameset',
84
+  'h1',
85
+  'head',
86
+  'header',
87
+  'hgroup',
88
+  'hr',
89
+  'html',
90
+  'i',
91
+  'iframe',
92
+  'image',
93
+  'img',
94
+  'input',
95
+  'ins',
96
+  'isindex',
97
+  'kbd',
98
+  'keygen',
99
+  'label',
100
+  'legend',
101
+  'li',
102
+  'link',
103
+  'listing',
104
+  'main',
105
+  'map',
106
+  'mark',
107
+  'marquee',
108
+  'menu',
109
+  'menuitem',
110
+  'meta',
111
+  'meter',
112
+  'multicol',
113
+  'nav',
114
+  'nextid',
115
+  'nobr',
116
+  'noembed',
117
+  'noframes',
118
+  'noscript',
119
+  'object',
120
+  'ol',
121
+  'optgroup',
122
+  'option',
123
+  'output',
124
+  'p',
125
+  'param',
126
+  'picture',
127
+  'plaintext',
128
+  'pre',
129
+  'progress',
130
+  'q',
131
+  'rb',
132
+  'rp',
133
+  'rt',
134
+  'rtc',
135
+  'ruby',
136
+  's',
137
+  'samp',
138
+  'script',
139
+  'section',
140
+  'select',
141
+  'shadow',
142
+  'slot',
143
+  'small',
144
+  'source',
145
+  'spacer',
146
+  'span',
147
+  'strike',
148
+  'strong',
149
+  'style',
150
+  'sub',
151
+  'summary',
152
+  'sup',
153
+  'table',
154
+  'tbody',
155
+  'td',
156
+  'template',
157
+  'textarea',
158
+  'tfoot',
159
+  'th',
160
+  'thead',
161
+  'time',
162
+  'title',
163
+  'tr',
164
+  'track',
165
+  'tt',
166
+  'u',
167
+  'ul',
168
+  'var',
169
+  'video',
170
+  'wbr',
171
+  'xm'
172
+]
173
+
174
+tags.forEach(tagName => {
175
+  core[tagName] = (...args) => element(tagName, args)
176
+})
177
+
178
+module.exports = core

+ 18
- 0
test/compilerTest.js View File

@@ -28,3 +28,21 @@ test('compiles applications that evaluate to numbers', t => {
28 28
   const result = helpers.compile('((lambda (x) (+ x 1)) 5)')
29 29
   t.equal(result, '6')
30 30
 })
31
+
32
+test('simple element', t => {
33
+  t.plan(1)
34
+  const result = helpers.compile('(div :class "container" "Lorem ipsum dolor sit amet")')
35
+  t.equal(result, '<div class="container">Lorem ipsum dolor sit amet</div>')
36
+})
37
+
38
+test('nested element', t => {
39
+  t.plan(1)
40
+  const result = helpers.compile('(div (span "Lorem ipsum dolor sit amet"))')
41
+  t.equal(result, '<div><span>Lorem ipsum dolor sit amet</span></div>')
42
+})
43
+
44
+test('self closing elements', t => {
45
+  t.plan(1)
46
+  const result = helpers.compile('(img :src "logo.png")')
47
+  t.equal(result, '<img src="logo.png">')
48
+})

+ 64
- 0
test/coreTest.js View File

@@ -51,3 +51,67 @@ test('map', t => {
51 51
     }),
52 52
   )
53 53
 })
54
+
55
+test('div', t => {
56
+  t.plan(1)
57
+  const tree = helpers.evaluate('(div :class "hello" "world")')
58
+  t.deepEqual(
59
+    tree[0],
60
+    new AST.HTMLElement({
61
+      name: 'div',
62
+      attributes: [
63
+        new AST.Attribute({
64
+          name: 'class',
65
+          value: new AST.String({ value: 'hello' }),
66
+        })
67
+      ],
68
+      contents: [ new AST.String({ value: 'world' }) ],
69
+      selfClosing: false,
70
+    })
71
+  )
72
+})
73
+
74
+test('nested elements', t => {
75
+  t.plan(1)
76
+  const tree = helpers.evaluate('(div :class "hello" (div :class "world"))')
77
+  t.deepEqual(
78
+    tree[0],
79
+    new AST.HTMLElement({
80
+      name: 'div',
81
+      attributes: [
82
+        new AST.Attribute({
83
+          name: 'class',
84
+          value: new AST.String({ value: 'hello' }),
85
+        }),
86
+      ],
87
+      contents: [
88
+        new AST.HTMLElement({
89
+          name: 'div',
90
+          attributes: [
91
+            new AST.Attribute({
92
+              name: 'class',
93
+              value: new AST.String({ value: 'world' }),
94
+            })
95
+          ],
96
+          contents: [],
97
+          selfClosing: false,
98
+        })
99
+      ],
100
+      selfClosing: false,
101
+    })
102
+  )
103
+})
104
+
105
+test('self closing tag', t => {
106
+  t.plan(1)
107
+  const tree = helpers.evaluate('(img)')
108
+  t.deepEqual(
109
+    tree[0],
110
+    new AST.HTMLElement({
111
+      name: 'img',
112
+      attributes: [],
113
+      contents: [],
114
+      selfClosing: true,
115
+    })
116
+  )
117
+})

Loading…
Cancel
Save