├── .gitignore
├── .travis.yml
├── LICENSE
├── README.txt
├── compilers
├── common.js
├── dom.js
└── html.js
├── docs
├── index.css
├── index.html
├── index.js
└── src
│ ├── header.min
│ ├── index.min
│ ├── index.styl
│ └── sections
│ ├── comments.min
│ ├── conditionals.min
│ ├── filters.min
│ ├── iteration.min
│ ├── mixins.min
│ ├── tags.min
│ ├── text.min
│ └── values.min
├── index.js
├── lexer.js
├── package.json
├── parser.js
└── test
├── browser.js
├── cache.js
├── cases.js
├── cases
├── attrs-data.html
├── attrs-data.min
├── attrs.html
├── attrs.min
├── basic.html
├── basic.min
├── blanks.html
├── blanks.min
├── classes-empty.html
├── classes-empty.min
├── comments.html
├── comments.min
├── conditionals.html
├── conditionals.min
├── escape.html
├── escape.min
├── format.html
├── format.min
├── nest.html
├── nest.min
├── pipeless-tag.html
└── pipeless-tag.min
├── fixtures
├── cached.min
├── mixins
│ ├── mixin.min
│ ├── withmixin.min
│ └── withoutmixin.min
├── output-dirs
│ ├── cli-input
│ │ ├── a
│ │ │ └── index.min
│ │ ├── b
│ │ │ └── index.min
│ │ └── c
│ │ │ └── index.min
│ └── cli-output
│ │ ├── a
│ │ └── index.html
│ │ ├── b
│ │ └── index.html
│ │ └── c
│ │ └── index.html
└── output-single
│ ├── cli-input
│ ├── a
│ │ └── a.min
│ └── index.min
│ └── cli-output
│ └── index.html
├── mixins.js
├── not-escaped.js
└── script-not-escaped.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .DS_Store
3 | /*.log
4 | /*.sw*
5 |
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | node_js:
4 | - "4.2"
5 | addons:
6 | apt:
7 | packages:
8 | - xvfb
9 | install:
10 | - export DISPLAY=':99.0'
11 | - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
12 | - npm install
13 |
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | =====================
3 |
4 | Copyright (c) 2012-2015 Voltra Co.
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
7 |
8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
9 |
10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 |
12 |
--------------------------------------------------------------------------------
/README.txt:
--------------------------------------------------------------------------------
1 | ABOUT
2 | Mineral is a language that compiles to markup or dom. It's similar to Jade (aka
3 | Pug). Its goal is to be much smaller and simpler than pug, and integrate well
4 | with modern client side frameworks.
5 |
6 | DOCS
7 | http://voltraco.github.io/mineral/
8 |
9 | INSTALL
10 | npm i mineral
11 |
12 | CLI
13 | Watch and compile mineral files https://github.com/voltraco/mineral-cli
14 |
15 | WEBPACK
16 | Use https://github.com/voltraco/mineral-loader to read mineral files and parse
17 | them into trees. This is easy, just `require('./file.min')`.
18 |
19 | BROWSERIFY
20 | For browserify, use the individual components.
21 |
22 | const tree = require('mineral/parser')(string)
23 | const el = require('mineral/compilers/dom')(tree)
24 |
25 |
--------------------------------------------------------------------------------
/compilers/common.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const fmt = require('util').format
3 | const path = require('path')
4 | const callsites = require('callsites')
5 |
6 | const parse = require('../parser')
7 |
8 | const CLASS_RE = /\.[^.]+/g
9 | const ID_RE = /#[^. ]+/g
10 |
11 | exports.unclosed = [
12 | 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
13 | 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr',
14 | 'doctype'
15 | ]
16 |
17 | exports.resolveTagOrSymbol = function resolveTagOrSymbol (string) {
18 | const props = {
19 | tagname: 'div',
20 | classname: [],
21 | id: '',
22 | content: ''
23 | }
24 |
25 | string = string.replace(ID_RE, function (id) {
26 | props.id = id.slice(1)
27 | return ''
28 | })
29 |
30 | string = string.replace(CLASS_RE, function (cls) {
31 | props.classname.push(cls.slice(1))
32 | return ''
33 | })
34 |
35 | if (string) props.tagname = string
36 | props.classname = props.classname.join(' ')
37 | return props
38 | }
39 |
40 | exports.resolveInclude = function resolver (info, shouldParse) {
41 | let dirname = info.location
42 |
43 | const cs = callsites()
44 |
45 | if (!dirname && cs.length) {
46 | for (let c of cs) {
47 | cs.shift()
48 | const f = c.getFileName()
49 | const index = f.indexOf('/mineral/index') > -1
50 | if (index) break
51 | }
52 |
53 | if (cs[1]) dirname = path.dirname(cs[1].getFileName())
54 | }
55 |
56 | let stat = null
57 |
58 | try {
59 | stat = fs.statSync(info.location)
60 | } catch (_) {
61 | }
62 |
63 | if (stat && !stat.isDirectory()) {
64 | dirname = path.dirname(info.location)
65 | }
66 |
67 | const location = path.resolve(dirname, info.path)
68 | const text = fs.readFileSync(location, 'utf8')
69 |
70 | return {
71 | tree: shouldParse ? parse(text) : text,
72 | location: location
73 | }
74 | }
75 |
76 | exports.die = function die (info, name, message) {
77 | const msg = fmt('%s:%d:%d', message, info.pos.column, info.pos.lineno)
78 | const err = new Error(msg)
79 | err.stack = ''
80 | err.name = name
81 | throw err
82 | }
83 |
84 | exports.scopedExpression = function scopedExpression (data, info, str) {
85 | const args = Object.keys(data).concat(['__format', 'return ' + str + ';'])
86 | const fn = Function.apply(null, args)
87 | const values = Object.keys(data).map(k => data[k])
88 | values.push(fmt)
89 |
90 | try {
91 | return fn.apply(data, values)
92 | } catch (ex) {
93 | console.warn('%s: %s in %s %s:%s',
94 | ex.name, ex.message, info.location, info.pos.column, info.pos.lineno)
95 | return ''
96 | }
97 | }
98 |
99 | exports.each = function each (o, f) {
100 | const has = Object.prototype.hasOwnProperty
101 | if (Array.isArray(o)) {
102 | for (let i = 0; i < o.length; ++i) f(o[i], i)
103 | } else {
104 | for (let k in o) if (has.call(o, k)) f(o[k], k)
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/compilers/dom.js:
--------------------------------------------------------------------------------
1 | const he = require('he')
2 | const common = require('./common')
3 | // const fmt = require('util').format
4 | const cache = {}
5 |
6 | const IF_RE = /^\s*if\s*/
7 | const IN_RE = /\s*in\s*/
8 | const FMT_RE = /["'](.*?)["'], /
9 | const EQ_RE = /^\s*=\s*/
10 |
11 | // const COLON = 58
12 | // const PLUS = 43
13 | const HYPHEN = 45
14 | const A = 65
15 | const Z = 90
16 |
17 | // *** experiment with creating dom nodes ***
18 |
19 | function dom (tree, node, data) {
20 | let findElseBranch = false
21 | let logical = false
22 |
23 | function getValue (data, info, str) {
24 | if (!EQ_RE.test(str)) return str
25 | let exp = str.replace(EQ_RE, '')
26 | if (FMT_RE.test(exp)) exp = '__format(' + exp + ')'
27 | logical = true
28 | const value = common.scopedExpression(data, info, exp)
29 | return value // he.escape(value + '')
30 | }
31 |
32 | function compile (child, index) {
33 | // if (child.dom) return node.appendChild(child.dom)
34 | const locationData = { pos: child.pos || {}, location }
35 |
36 | // if (child.unescaped) {
37 | // child.content = he.escape(child.content)
38 | // }
39 |
40 | const firstLetter = child.tagOrSymbol.charCodeAt(0)
41 |
42 | //
43 | // first handle any flow control statements
44 | //
45 | if (child.tagOrSymbol === 'else') {
46 | if (!findElseBranch) return
47 |
48 | logical = true
49 | // if this is an else-if
50 | if (IF_RE.test(child.content)) {
51 | const exp = child.content.replace(IF_RE, '')
52 | if (common.scopedExpression(data, locationData, exp)) {
53 | findElseBranch = false
54 | const branch = document.createDocumentFragment()
55 | const children = dom(child, branch, data)
56 | if (children) node.appendChild(branch)
57 | }
58 | return
59 | }
60 |
61 | findElseBranch = false
62 | const branch = document.createDocumentFragment()
63 | const children = dom(child, branch, data)
64 | if (children) node.appendChild(branch)
65 | return
66 | }
67 |
68 | if (child.tagOrSymbol === 'comment') {
69 | const comment = document.createComment(child.content)
70 | node.appendChild(comment)
71 | return
72 | }
73 |
74 | if (child.tagOrSymbol === 'if') {
75 | logical = true
76 | if (common.scopedExpression(data, locationData, child.content)) {
77 | const branch = document.createDocumentFragment()
78 | const children = dom(child, branch, data)
79 | if (children) node.appendChild(branch)
80 | return
81 | }
82 | findElseBranch = true
83 | return
84 | }
85 |
86 | if (child.tagOrSymbol === 'while') {
87 | logical = true
88 | while (common.scopedExpression(data, locationData, child.content)) {
89 | const children = dom(child, node, data)
90 | if (children) node.appendChild(children)
91 | }
92 | return
93 | }
94 |
95 | if (child.tagOrSymbol === 'for') {
96 | logical = true
97 |
98 | const parts = child.content.split(IN_RE)
99 | if (!parts[0]) common.die(locationData, 'TypeError', 'Unknown mixin')
100 |
101 | const object = common.scopedExpression(data, locationData, parts[1])
102 |
103 | common.each(object, function (val, key) {
104 | // determine if there are identifiers for key and value
105 | const identifiers = parts[0].split(',')
106 | const keyIdentifier = identifiers[0]
107 | const valIdentifier = identifiers[1]
108 | const locals = { [keyIdentifier]: key }
109 |
110 | if (valIdentifier) {
111 | locals[valIdentifier] = val
112 | }
113 | // create a new shallow scope so that locals don't persist
114 | const scope = Object.assign({}, data, locals)
115 | const children = dom(child, node, scope)
116 | if (children) node.appendChild(children)
117 | })
118 | return
119 | }
120 |
121 | if (child.tagOrSymbol === 'each') {
122 | common.die(locationData, 'TypeError', 'Each not supported (use for)')
123 | }
124 |
125 | // treat all piped text as plain content
126 | if (child.tagOrSymbol === '|') {
127 | return (node.textContent += getValue(data, locationData, child.content))
128 | }
129 |
130 | if (firstLetter === HYPHEN) {
131 | common.die(locationData, 'Error', 'No inline code!')
132 | }
133 |
134 | /* if (firstLetter === COLON && cb) {
135 | logical = true
136 | const name = 'jstransformer-' + child.tagOrSymbol.slice(1)
137 | let t = null
138 | try {
139 | t = transformer(require(name))
140 | } catch (ex) {
141 | common.die(child.pos, 'Error', fmt('%s not installed', name))
142 | }
143 | const path = child.content
144 | const data = cb({ path, location })
145 | const parsed = t.render(data.tree, child.attributes)
146 | node.textContent += parsed.body
147 | return
148 | }
149 |
150 | // anything prefixed with '+' is a mixin call.
151 | if (firstLetter === PLUS && cb) {
152 | logical = true
153 | const name = child.tagOrSymbol.slice(1)
154 | if (!cache[name]) {
155 | common.die(child.pos, 'TypeError', 'Unknown mixin')
156 | }
157 |
158 | const locals = {}
159 | const args = Object.keys(child.attributes).map(attr => {
160 | return common.scopedExpression(data, child.pos, attr)
161 | })
162 |
163 | cache[name].keys.map((k, index) => (locals[k] = args[index]))
164 | const scope = Object.assign({}, data, locals)
165 | const children = dom(cache[name].child, node, scope)
166 | node.appendChild(children)
167 | return
168 | } */
169 |
170 | // defines a mixin
171 | if (firstLetter >= A && firstLetter <= Z) {
172 | logical = true
173 | const keys = Object.keys(child.attributes)
174 | cache[child.tagOrSymbol] = { child, keys }
175 | return
176 | }
177 |
178 | //
179 | // everything else is a tag
180 | //
181 | const props = common.resolveTagOrSymbol(child.tagOrSymbol)
182 |
183 | let el = document.createElement(props.tagname)
184 |
185 | if (props.id) el.id = props.id
186 | if (props.classname) el.className = props.classname
187 |
188 | if (child.attributes) {
189 | Object.keys(child.attributes).map(key => {
190 | let value = child.attributes[key]
191 |
192 | // if this attribute is a boolean, make its value its key
193 | if (typeof value === 'boolean') {
194 | el.setAttribute(key, key)
195 | }
196 |
197 | if (value) {
198 | // all values are expressions
199 | value = common.scopedExpression(data, locationData, value)
200 | // a class should not be empty
201 | if (key === 'class' && !value) return
202 |
203 | // data-* attributes should be escaped
204 | if (key.indexOf('data-') === 0) {
205 | if (typeof value !== 'string' && typeof value !== 'number') {
206 | value = JSON.stringify(value)
207 | }
208 | }
209 | }
210 | el.setAttribute(key, value)
211 | })
212 | }
213 |
214 | if (child.content) {
215 | el.textContent = (child.tagOrSymbol === 'script')
216 | ? child.content
217 | : getValue(data, locationData, child.content)
218 | }
219 |
220 | // nothing left to decide, recurse if there are child nodes
221 | if (child.children.length) {
222 | const children = dom(child, el, data)
223 | if (children) node.appendChild(children)
224 | }
225 |
226 | // if this is not a logical node, we can make an optimization
227 | // if (!logical) child.dom = el
228 | node.appendChild(el)
229 | }
230 |
231 | if (tree.children && tree.children.length) {
232 | tree.children.map(compile)
233 | }
234 | return node
235 | }
236 |
237 | module.exports = function compiler (tree, data) {
238 | data = data || {}
239 | const node = document.createDocumentFragment()
240 | return dom(tree, node, data)
241 | }
242 |
243 |
--------------------------------------------------------------------------------
/compilers/html.js:
--------------------------------------------------------------------------------
1 | const transformer = require('jstransformer')
2 | const he = require('he')
3 | const resFrom = require('resolve-from')
4 | const common = require('./common')
5 | const fmt = require('util').format
6 |
7 | global.cache = {}
8 |
9 | const IF_RE = /^\s*if\s*/
10 | const IN_RE = /\s*in\s*/
11 | const FMT_RE = /["'](.*?)["'], /
12 | const MIN_FILE_RE = /\.min$/
13 | const EQ_RE = /^\s*=\s*/
14 |
15 | const COLON = 58
16 | const PLUS = 43
17 | const HYPHEN = 45
18 | const A = 65
19 | const Z = 90
20 |
21 | function html (tree, data, location, cb) {
22 | let findElseBranch = false
23 | let logical = false
24 |
25 | function getValue (data, info, str) {
26 | if (!EQ_RE.test(str)) return str
27 | let exp = str.replace(EQ_RE, '')
28 | if (FMT_RE.test(exp)) exp = '__format(' + exp + ')'
29 | logical = true
30 | return common.scopedExpression(data, info, exp)
31 | }
32 |
33 | function unescapedValue (data, info, str) {
34 | return getValue(data, info, str)
35 | }
36 |
37 | // determine if this is a path or just regular content
38 | function escapedValue (data, info, str) {
39 | return he.escape(getValue(data, info, str) + '')
40 | }
41 |
42 | function compile (child, index) {
43 | // if (child.html) return child.html
44 | const locationData = { pos: child.pos || {}, location }
45 |
46 | // if (child.unescaped) {
47 | // child.content = child.content
48 | // }
49 |
50 | const firstLetter = child.tagOrSymbol.charCodeAt(0)
51 | //
52 | // first handle any flow control statements
53 | //
54 | if (child.tagOrSymbol === 'else') {
55 | if (!findElseBranch) return ''
56 |
57 | logical = true
58 | // if this is an else-if
59 | if (IF_RE.test(child.content)) {
60 | const exp = child.content.replace(IF_RE, '')
61 | if (common.scopedExpression(data, locationData, exp)) {
62 | findElseBranch = false
63 | return html(child, data, location, cb)
64 | }
65 | return ''
66 | }
67 |
68 | findElseBranch = false
69 | return html(child, data, location, cb)
70 | }
71 |
72 | if (child.tagOrSymbol === 'comment') {
73 | return ''
74 | }
75 |
76 | if (child.tagOrSymbol === 'if') {
77 | logical = true
78 | if (common.scopedExpression(data, locationData, child.content)) {
79 | return html(child, data, location, cb)
80 | }
81 | findElseBranch = true
82 | return ''
83 | }
84 |
85 | if (child.tagOrSymbol === 'while') {
86 | logical = true
87 | let value = ''
88 | while (common.scopedExpression(data, locationData, child.content)) {
89 | value += html(child, data, location, cb)
90 | }
91 | return value
92 | }
93 |
94 | if (child.tagOrSymbol === 'for') {
95 | logical = true
96 |
97 | const parts = child.content.split(IN_RE)
98 | if (!parts[0]) common.die(locationData, 'TypeError', 'Not enough arguments')
99 |
100 | const object = common.scopedExpression(data, locationData, parts[1])
101 |
102 | let value = ''
103 | common.each(object, function (val, key) {
104 | // determine if there are identifiers for key and value
105 | const identifiers = parts[0].split(',')
106 | const keyIdentifier = identifiers[0]
107 | const valIdentifier = identifiers[1]
108 | const locals = { [keyIdentifier]: key }
109 |
110 | if (valIdentifier) {
111 | locals[valIdentifier] = val
112 | }
113 | // create a new shallow scope so that locals don't persist
114 | const scope = Object.assign({}, data, locals)
115 | value += html(child, scope, location, cb)
116 | })
117 | return value
118 | }
119 |
120 | if (child.tagOrSymbol === 'each') {
121 | common.die(locationData, 'TypeError', 'Each not supported (use for)')
122 | }
123 |
124 | // treat all piped text as plain content
125 | if (child.tagOrSymbol === '|') {
126 | return (' ' + escapedValue(data, locationData, child.content))
127 | }
128 |
129 | if (child.tagOrSymbol === '!') {
130 | return (' ' + unescapedValue(data, locationData, child.content))
131 | }
132 |
133 | if (firstLetter === HYPHEN) {
134 | common.die(locationData, 'Error', 'No inline code!')
135 | }
136 |
137 | if (firstLetter === COLON && cb) {
138 | logical = true
139 | const name = 'jstransformer-' + child.tagOrSymbol.slice(1)
140 | let t = null
141 |
142 | try {
143 | t = transformer(require(resFrom('.', name)))
144 | } catch (ex) {
145 | const msg = fmt('%s could not load (%s)', name, ex.message)
146 | common.die(locationData, 'Error', msg)
147 | }
148 | const path = child.content
149 | const data = cb({ path, location })
150 | const parsed = t.render(data.tree, child.attributes)
151 | return parsed.body
152 | }
153 |
154 | // anything prefixed with '+' is a mixin call.
155 | if (firstLetter === PLUS && cb) {
156 | logical = true
157 | const name = child.tagOrSymbol.slice(1)
158 | if (!global.cache[name]) {
159 | const msg = fmt('Unknown mixin (%s) in %s', name, location)
160 | common.die(locationData, 'Error', msg)
161 | }
162 |
163 | const locals = {}
164 | const args = Object.keys(child.attributes).map(attr => {
165 | return common.scopedExpression(data, locationData, attr)
166 | })
167 |
168 | global.cache[name].keys.map((k, index) => (locals[k] = args[index]))
169 | const scope = Object.assign({}, data, locals)
170 | return html(global.cache[name].child, scope, location, cb)
171 | }
172 |
173 | // defines a mixin
174 | if (firstLetter >= A && firstLetter <= Z) {
175 | logical = true
176 | const keys = Object.keys(child.attributes)
177 | global.cache[child.tagOrSymbol] = { child, keys }
178 | return ''
179 | }
180 |
181 | if (child.tagOrSymbol === 'include') {
182 | // pass location to the cb so includes can be relative
183 | logical = true
184 | const path = child.content
185 |
186 | if (global.watcher) {
187 | global.addToWatcher(location, path)
188 | }
189 |
190 | // if the include is not a .min extension, it's plain text
191 | if (MIN_FILE_RE.test(path)) {
192 | const resolved = cb({ path, location }, true)
193 | return html(resolved.tree, data, resolved.location, cb)
194 | }
195 | return cb({ path, location })
196 | }
197 |
198 | //
199 | // everything else is a tag
200 | //
201 | const props = common.resolveTagOrSymbol(child.tagOrSymbol)
202 |
203 | let tag = ['<', props.tagname]
204 |
205 | if (props.id) {
206 | tag.push(' id="', props.id, '"')
207 | }
208 |
209 | if (props.classname) {
210 | tag.push(' class="', props.classname, '"')
211 | }
212 |
213 | if (child.attributes) {
214 | let attrs = Object.keys(child.attributes).map(key => {
215 | let value = child.attributes[key]
216 |
217 | // if this attribute is a boolean, make its value its key
218 | if (typeof value === 'boolean') {
219 | return [key, '=', `"${key}"`].join('')
220 | }
221 |
222 | if (value) {
223 | // all values are expressions
224 | value = common.scopedExpression(data, locationData, value)
225 |
226 | // a class should not be empty
227 | if (key === 'class' && !value) return ''
228 |
229 | // data-* attributes should be escaped
230 | if (key.indexOf('data-') === 0) {
231 | if (typeof value !== 'string' && typeof value !== 'number') {
232 | value = he.escape(JSON.stringify(value) + '')
233 | } else {
234 | value = '"' + value + '"'
235 | }
236 | } else {
237 | value = JSON.stringify(value)
238 | }
239 | }
240 | return [key, '=', value].join('')
241 | })
242 | attrs = attrs.filter(a => !!a)
243 | if (attrs.length) tag.push(' ', attrs.join(' '))
244 | }
245 |
246 | tag.push('>') // html5 doesn't care about self closing tags
247 |
248 | if (child.content) {
249 | const isScript = props.tagname === 'script'
250 | tag.push(isScript
251 | ? child.content
252 | : escapedValue(data, locationData, child.content)
253 | )
254 | }
255 |
256 | // nothing left to decide, recurse if there are child nodes
257 | if (child.children.length) {
258 | tag.push(html(child, data, location, cb))
259 | }
260 |
261 | // decide if the tag needs to be closed or not
262 | if (common.unclosed.indexOf(props.tagname) === -1) {
263 | tag.push('', props.tagname, '>')
264 | }
265 |
266 | var s = tag.join('')
267 | // if this is not a logical node, we can make an optimization
268 | // if (!logical) child.html = s
269 | return s
270 | }
271 |
272 | if (tree.children && tree.children.length) {
273 | return tree.children.map(compile).join('')
274 | }
275 | return ''
276 | }
277 |
278 | module.exports = function (tree, data, location, cb) {
279 | cb = cb || common.resolveInclude
280 | data = data || {}
281 | return html(tree, data, location, cb)
282 | }
283 |
--------------------------------------------------------------------------------
/docs/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Open Sans', sans-serif;
3 | font-size: 1em;
4 | padding: 20%;
5 | }
6 | h1,
7 | h2,
8 | h3,
9 | h4 {
10 | font-family: 'Source Sans Pro', sans-serif;
11 | }
12 | h1 {
13 | font-size: 2.5em;
14 | }
15 | h2 {
16 | margin-top: 100px;
17 | }
18 | a,
19 | a:visited,
20 | .red {
21 | color: #ff5252;
22 | }
23 | a {
24 | text-decoration: none;
25 | padding-bottom: 2px;
26 | border-bottom: 1px;
27 | outline: none;
28 | }
29 | p {
30 | line-height: 23px;
31 | }
32 | pre,
33 | code {
34 | font-family: 'Source Code Pro', monospace;
35 | font-size: 1em;
36 | overflow: auto;
37 | }
38 | pre {
39 | position: relative;
40 | padding: 10px;
41 | background-color: #fafafa;
42 | border-bottom: 1px solid #d5d5d5;
43 | }
44 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 | Mineral
mineral
Mineral is a language that compiles to markup or dom. It's similar to Jade (aka Pug). Its goal is to be much smaller and simpler than pug, and integrate well with modern client side frameworks.
Lowercase text at the start of a line (or after only white space) is considered an html tag. Indenting a tag will nest it, creating the tree-like structure that can be rendered into html. Tag attributes look similar to html (with optional comma), but their values are regular javascript.
html
137 | head
138 | link(href="index.css" rel="stylesheet")
139 | body
140 | h1 Hello World
141 |
All html is lowercase, anything starting with an uppercase letter is a mixin. arguments and parens are optional. mixins are global (less fiddling with paths).
Conditional statements (`if`, `else if` and `else`) have no parenthesis. They will evaluate a javascript expression to determine truthyness. Mineral does not implement `case` or `unless` statements.
if 1 === 'x'
159 | p this statement is false
160 | else if 100 > 99
161 | p this statement is also false, and optional
162 | else
163 | p this is the final branch of this logic
164 |
165 |
Iterate over objects or arrays using `for` or `while`. Mineral does not implelent the `each` statement. Here are some examples using the following JSON data.
When the `=` symbol follows a tag, it indicates that the value should be an expression. An expression can include values from the data passed to the template or any valid javascript.
To continue the text content of a tag, you can use the `|` symbol.
p Danzig is an American heavy metal band, formed in 1987 in Lodi,
182 | | New Jersey, United States. The band is the musical outlet for singer
183 | | and songwriter Glenn Danzig, preceded by the horror punk bands the
184 | | Misfits and Samhain. They play in a bluesy doom-driven heavy metal
185 | | style influenced by the early sound of Black Sabbath.[1]
186 |
For multiline textblocks, add a `.` after the tag. The compiler wont evaluate any code inside these blocks. It will also preserve whitespace.
note Comments have no business in compiled code, so they are removed entirely. Conditional comments are no longer supported by any IE version, so they wont be supported. If for some reason you really want comments, you can use the comment tag.
comment Hey good lookin'
<!-- Hey good lookin' -->
205 |
--------------------------------------------------------------------------------
/docs/index.js:
--------------------------------------------------------------------------------
1 | var requestAnimFrame = (function () {
2 | return window.requestAnimationFrame ||
3 | window.webkitRequestAnimationFrame ||
4 | window.mozRequestAnimationFrame ||
5 | function (callback) {
6 | window.setTimeout(callback, 1000 / 60)
7 | }
8 | })()
9 |
10 | function ease (pos) {
11 | if ((pos /= 0.5) < 1) {
12 | return 0.5 * Math.pow(pos, 5)
13 | }
14 | return 0.5 * (Math.pow((pos - 2), 5) + 2)
15 | }
16 |
17 | var scrolling = false
18 |
19 | function scrollToY (scrollTargetY, speed) {
20 | var scrollY = window.scrollY
21 | var currentTime = 0
22 | var pos = Math.abs(scrollY - scrollTargetY)
23 | var time = Math.max(0.1, Math.min(pos / speed, 0.8))
24 |
25 | function nextFrame () {
26 | currentTime += 1 / 60
27 | scrolling = true
28 |
29 | var p = currentTime / time
30 | var t = ease(p)
31 |
32 | if (p < 1) {
33 | requestAnimFrame(nextFrame)
34 | window.scrollTo(0, scrollY + ((scrollTargetY - scrollY) * t))
35 | } else {
36 | window.scrollTo(0, scrollTargetY)
37 | scrolling = false
38 | }
39 | }
40 | nextFrame()
41 | }
42 |
43 | var links = [].slice.call(document.querySelectorAll('li a'))
44 | var ranges = []
45 | var current
46 |
47 | links.map(function (link) {
48 | var id = link.getAttribute('href').slice(1)
49 | var section = document.getElementById(id)
50 |
51 | ranges.push({
52 | upper: section.offsetTop,
53 | lower: section.offsetTop + section.offsetHeight,
54 | id: id,
55 | link: link
56 | })
57 |
58 | var delay
59 |
60 | link.addEventListener('click', function (event) {
61 | clickInvoked = true
62 | event.preventDefault()
63 |
64 | var prev = document.querySelector('a.active')
65 | if (prev) prev.className = ''
66 | link.className = 'active'
67 | scrollToY(section.offsetTop, 1500)
68 | })
69 | })
70 |
71 | var nav = document.querySelector('nav')
72 | var hamburger = document.querySelector('.hamburger')
73 | var showAfterClick = false
74 | var lastpos
75 |
76 | hamburger.addEventListener('click', function () {
77 | nav.style.opacity = 1
78 | hamburger.style.zIndex = -1
79 | hamburger.style.opacity = 0
80 | showAfterClick = true
81 | })
82 |
83 | function onscroll (event) {
84 | if (scrolling) return
85 |
86 | var pos = document.body.scrollTop
87 |
88 | if (pos <= 90 && pos >= 0 && !showAfterClick) {
89 | nav.style.opacity = (pos / 100)
90 | }
91 |
92 | if (pos <= 0) {
93 | hamburger.style.zIndex = 20
94 | hamburger.style.opacity = 1
95 | }
96 |
97 | if (pos < lastpos) {
98 | showAfterClick = false
99 | }
100 |
101 | if (pos > 100) {
102 | showAfterClick = false
103 | nav.style.opacity = .9
104 | }
105 |
106 | lastpos = pos
107 | pos = pos + 100
108 |
109 | ranges.map(function (range) {
110 | if (pos >= range.upper && pos <= range.lower) {
111 | if (range.id === current) return
112 | current = range.id
113 | var prev = document.querySelector('a.active')
114 | if (prev) prev.className = ''
115 | range.link.className = 'active'
116 | }
117 | })
118 | }
119 |
120 | window.addEventListener('scroll', onscroll)
121 |
122 |
--------------------------------------------------------------------------------
/docs/src/header.min:
--------------------------------------------------------------------------------
1 | head
2 | link(
3 | href="https://fonts.googleapis.com/css?family=Open+Sans|Source+Code+Pro|Source+Sans+Pro"
4 | rel="stylesheet"
5 | )
6 | style
7 | :stylus ./index.styl
8 |
9 | // Title
10 | title Mineral
11 |
12 | meta(name="description" content="A terse language that compiles to html and dom trees")
13 |
14 | meta(name="keywords" content="templates, templating, javascript, js, jade, jade-lang, pug, pugjs, pug-js, mineraljs, mineral-lang, mineral-js")
15 | meta(http-equiv='Content-Type', content='text/html; charset=utf-8')
16 | meta(name="viewport" content="width=device-width, initial-scale=1")
17 |
18 | // Viewport
19 | meta(name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no")
20 |
21 | // Favicon
22 | link(rel="icon" type="image/png" href="https://voltra.co/favicon-32x32.png" sizes="32x32")
23 | link(rel="icon" type="image/png" href="https://voltra.co/favicon-16x16.png" sizes="16x16")
24 |
25 |
--------------------------------------------------------------------------------
/docs/src/index.min:
--------------------------------------------------------------------------------
1 | html
2 |
3 | include ./header.min
4 |
5 | body
6 | header
7 | h1 mineral
8 | .hamburger
9 | .inner
10 | nav
11 | em mineral
12 | ul
13 | li
14 | a.active(href="#tags") tags
15 | li
16 | a(href="#mixins") mixins
17 | li
18 | a(href="#conditionals") conditionals
19 | li
20 | a(href="#iteration") iteration
21 | li
22 | a(href="#values") values
23 | li
24 | a(href="#text") text
25 | li
26 | a(href="#filters") filters
27 | li
28 | a(href="#comments") comments
29 |
30 | p
31 | a(href="https://github.com/voltraco/mineral") Mineral
32 | | is a language that compiles to markup or dom. It's similar to Jade (aka
33 | | Pug). Its goal is to be much smaller and simpler than pug, and integrate
34 | | well with modern client side frameworks.
35 |
36 | include ./sections/tags.min
37 | include ./sections/mixins.min
38 | include ./sections/conditionals.min
39 | include ./sections/iteration.min
40 | include ./sections/values.min
41 | include ./sections/text.min
42 | include ./sections/filters.min
43 | include ./sections/comments.min
44 |
45 | script(src="./index.js")
46 |
--------------------------------------------------------------------------------
/docs/src/index.styl:
--------------------------------------------------------------------------------
1 | $red = #ff5252
2 | $gray = #333
3 | $lightgray = rgba(0,0,0,0.14)
4 |
5 | body
6 | font-family 'Open Sans', sans-serif
7 | font-size 1em
8 | max-width 800px
9 | margin 15% auto
10 | padding 5%
11 |
12 | section
13 | padding-top 30px
14 |
15 | h1, h2, h3, h4
16 | font-family 'Source Sans Pro', sans-serif
17 |
18 | h1
19 | font-size 2em
20 | vertical-align bottom
21 | display inline
22 | text-transform uppercase
23 | line-height 1.6em
24 |
25 | h2
26 | border 2px solid $red
27 | padding 4px 8px 5px 8px
28 | display inline-block
29 | text-transform uppercase
30 | letter-spacing .05em
31 | font-size 1em
32 | margin-top 100px
33 |
34 | a, a:visited, .red
35 | color $red
36 |
37 | a
38 | text-decoration none
39 | padding-bottom 2px
40 | border-bottom 1px
41 | outline none
42 |
43 | p
44 | line-height 23px
45 |
46 | pre, code
47 | font-family 'Source Code Pro', monospace
48 | font-size 1em
49 | overflow auto
50 |
51 | pre
52 | position relative
53 | padding 10px
54 | background-color #fafafa
55 | border-bottom 1px solid #d5d5d5
56 | &.warn
57 | border-bottom 1px solid red
58 |
59 | .hamburger
60 | opacity 1
61 | transition all .4s
62 | position absolute
63 | top 20px
64 | right 20px
65 | width 26px
66 | height 20px
67 | border-top 1px solid $gray
68 | border-bottom 1px solid $gray
69 | z-index 20
70 | .inner
71 | height 10px
72 | border-bottom 1px solid $gray
73 |
74 | nav
75 | opacity 0
76 | z-index 10
77 | position fixed
78 | top 0
79 | left 0
80 | right 0
81 | height 60px
82 | background white
83 | border-bottom 1px solid $lightgray
84 | box-shadow 0px 2px 3px $lightgray
85 |
86 | em
87 | font-style normal
88 | display inline-block
89 | margin 16px 20px
90 | font-size 21px
91 | text-transform uppercase
92 | font-family 'Source Sans Pro', sans-serif
93 |
94 | ul
95 | display inline-block
96 | float right
97 | margin-right 10px
98 | list-style none
99 | li
100 | float left
101 |
102 | a, a:visited
103 | transition all .4s
104 | color $gray
105 | font-size 12px
106 | text-transform uppercase
107 | margin 14px
108 | text-decoration none
109 | border 1px solid transparent
110 | letter-spacing .03em
111 | padding 4px
112 |
113 | &:hover, &.active
114 | color $red
115 |
116 | &.active
117 | border 1px solid
118 | padding 4px
119 |
120 | @media screen and (max-width: 900px)
121 | nav, .hamburger
122 | display none
123 |
124 |
--------------------------------------------------------------------------------
/docs/src/sections/comments.min:
--------------------------------------------------------------------------------
1 | section#comments
2 |
3 | a(href="#comments")
4 | h2 comments
5 |
6 | p Single-line
7 |
8 | pre.input
9 | code.
10 | // single line
11 |
12 | p Multi-line (`.beep` and `.boop` are commented)
13 |
14 | pre.input
15 | code.
16 | .foo1
17 | //
18 | .beep
19 | .boop
20 | .foo2
21 |
22 | p
23 | span.red note
24 | | Comments have no business in compiled code, so they are removed entirely.
25 | | Conditional comments are no longer supported by any IE version, so they
26 | | wont be supported. If for some reason you really want comments, you can
27 | | use the comment tag.
28 | pre.input
29 | code.
30 | comment Hey good lookin'
31 | pre.output
32 | code.
33 |
34 |
--------------------------------------------------------------------------------
/docs/src/sections/conditionals.min:
--------------------------------------------------------------------------------
1 | section#conditionals
2 |
3 | a(href="#conditionals")
4 | h2 conditionals
5 |
6 | p Conditional statements (`if`, `else if` and `else`) have no parenthesis.
7 | | They will evaluate a javascript expression to determine truthyness.
8 | | Mineral does not implement `case` or `unless` statements.
9 |
10 | pre.input
11 | code.
12 | if 1 === 'x'
13 | p this statement is false
14 | else if 100 > 99
15 | p this statement is also false, and optional
16 | else
17 | p this is the final branch of this logic
18 |
19 |
--------------------------------------------------------------------------------
/docs/src/sections/filters.min:
--------------------------------------------------------------------------------
1 | section#filters
2 |
3 | a(href="#filters")
4 | h2 filters
5 |
6 | p Use any
7 | a(href="https://www.npmjs.com/browse/keyword/jstransformer") jstramsformer
8 | | module. For example, `npm install --save jstransformer-marked`. Arguments
9 | | and parens are optional.
10 |
11 | pre.input
12 | code.
13 | :marked(gfm=true) ./readme.md
14 |
--------------------------------------------------------------------------------
/docs/src/sections/iteration.min:
--------------------------------------------------------------------------------
1 | section#iteration
2 | a(href="#iteration")
3 | h2 iteration
4 |
5 | p Iterate over objects or arrays using `for` or `while`. Mineral does not
6 | | implelent the `each` statement. Here are some examples using the
7 | | following JSON data.
8 |
9 | pre.input
10 | code.
11 | {
12 | count: 2,
13 | people: [
14 | { first: 'Tom', last: 'Waits' },
15 | { first: 'Dick', last: 'Dale' }
16 | ]
17 | }
18 |
19 | p A for loop provides the key (or index) and optionally the value for each
20 | | item in an array or object.
21 | pre.input
22 | code.
23 | for key, val in people
24 | +Foo(val.first, val.last)
25 |
26 | p The value variable is optional.
27 | pre.input
28 | code.
29 | for p in people
30 | +Foo(people[p].first, people[p].last)
31 |
32 | p while loops evaluate a javascript expression until it becomes falsey.
33 | pre.input
34 | code.
35 | ul
36 | while count--
37 | span.name= people[count].first
38 |
--------------------------------------------------------------------------------
/docs/src/sections/mixins.min:
--------------------------------------------------------------------------------
1 | section#mixins
2 | a(id="mixins" href="#mixins")
3 | h2 mixins
4 |
5 | p All html is lowercase, anything starting with an uppercase letter is a
6 | | mixin. arguments and parens are optional. mixins are global (less fiddling
7 | | with paths).
8 |
9 | pre.input
10 | code.
11 | Person(firstName, lastName)
12 | h1= firstName
13 | h2= lastName
14 |
15 | p use mixins by putting a `+` before the name of the mixin. Arguments and
16 | | parens are optional.
17 |
18 | pre.input
19 | code.
20 | +Person('Jello', 'Biafra')
21 |
--------------------------------------------------------------------------------
/docs/src/sections/tags.min:
--------------------------------------------------------------------------------
1 | section#tags
2 |
3 | a(href="#tags")
4 | h2 tags
5 |
6 | p Lowercase text at the start of a line (or after only white space) is
7 | | considered an html tag. Indenting a tag will nest it, creating the
8 | | tree-like structure that can be rendered into html. Tag attributes
9 | | look similar to html (with optional comma), but their values are regular
10 | | javascript.
11 |
12 | pre.input
13 | code.
14 | html
15 | head
16 | link(href="index.css" rel="stylesheet")
17 | body
18 | h1 Hello World
19 |
20 | p the above code will produce the following html
21 | pre.output
22 | code.
23 |
24 |
25 |
26 |
27 |
28 |
Hello World
29 |
30 |
31 |
32 | p
33 | span.red note
34 | | It's ideal to disambiguate expressions or declarations when using them as
35 | | attribute values.
36 | pre.warn
37 | code.
38 | a(href='/save' data-date=(new Date()))
39 |
40 | br
41 | p A tag that starts with a dot will be considered a div, and the name will
42 | | be used as the class.
43 | pre
44 | code.
45 | .band The Misfits
46 | pre
47 | code.
48 |
The Misfits
49 | p
50 | span.red note
51 | | Unlike Jade or Pug, Mineral does not support the (rather superfluous)
52 | | class-after-attribute syntax.
53 | pre.warn
54 | code.
55 | a(href='/save').button save // Bad
56 | a.button(href='/save') // Good
57 |
58 | br
59 | p A tag that starts with a hash will be considered a div, and the name will
60 | | be used as the id. It's fine for the id to come before or after the class.
61 | pre
62 | code.
63 | #danzig.band The Misfits
64 | pre
65 | code.
66 |
The Misfits
67 |
68 |
--------------------------------------------------------------------------------
/docs/src/sections/text.min:
--------------------------------------------------------------------------------
1 | section#text
2 | a(id="text" href="#text")
3 | h2 text
4 |
5 | p To continue the text content of a tag, you can use the `|` symbol.
6 |
7 | pre.input
8 | code.
9 | p Danzig is an American heavy metal band, formed in 1987 in Lodi,
10 | | New Jersey, United States. The band is the musical outlet for singer
11 | | and songwriter Glenn Danzig, preceded by the horror punk bands the
12 | | Misfits and Samhain. They play in a bluesy doom-driven heavy metal
13 | | style influenced by the early sound of Black Sabbath.[1]
14 |
15 | p For multiline textblocks, add a `.` after the tag. The compiler wont
16 | | evaluate any code inside these blocks. It will also preserve whitespace.
17 |
18 | pre.input
19 | code.
20 | .foo.
21 | Hello
22 | world.
23 |
24 | pre.output
25 | code.
26 |
27 | Hello
28 | world.
29 |
30 |
31 | p This feature is pretty important when adding javascript to a script tag.
32 |
33 | pre.input
34 | code.
35 | script.
36 | var s = 'hello, world'
37 | alert(s)
38 | h1 I sent you an alert.
39 |
--------------------------------------------------------------------------------
/docs/src/sections/values.min:
--------------------------------------------------------------------------------
1 | section#values
2 |
3 | a(id="values" href="#values")
4 | h2 values
5 |
6 | p When the `=` symbol follows a tag, it indicates that the value should be an
7 | | expression. An expression can include values from the data passed to the
8 | | template or any valid javascript.
9 |
10 | pre.input
11 | code.
12 | h1 = 'Hello, ' + name
13 |
14 | p Mineral detects `console.log`-like statements.
15 | pre.input
16 | code.
17 | h1 = 'Hello %s', foo
18 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const fs = require('fs')
3 |
4 | const parse = require('./parser')
5 | const html = require('./compilers/html')
6 |
7 | const root = path.dirname(arguments[2].parent.filename)
8 | delete require.cache[__filename]
9 |
10 | exports.readFileSync = f => {
11 | const location = path.join(root, f)
12 | const source = fs.readFileSync(location, 'utf8')
13 | const tree = parse(source)
14 | return data => html(tree, data, root)
15 | }
16 |
--------------------------------------------------------------------------------
/lexer.js:
--------------------------------------------------------------------------------
1 | const QUOTED_RE = /^"|'/
2 | const WORD_RE = /^[^,\n\r =]+/
3 | const DELIMITER_RE = /^\s*=\s*/
4 | const SEP_RE = /^,/
5 | const EOV_RE = /^\s+[$_A-Za-z]+/ // possible end of value
6 | const WHITESPACE_RE = /^[^\S\x0a\x0d]*/ // but not new lines
7 | const ANYSPACE_RE = /^\s*/ // any whitespace
8 | const NON_WHITESPACE_RE = /^\S+/ // any non whitespace
9 | const TAGORSYMBOL_RE = /[\.#]?[^=(\s]*/ // any valid tag or symbol
10 | const NEWLINE_RE = /[\x0a\x0d]+/ // the next NL
11 |
12 | //
13 | // Lexer() provides a simple API that can read and reduce a source stream.
14 | // each method is either a helper or a matcher that represents a token.
15 | //
16 | module.exports = function Lexer (str, options) {
17 | options = options || {}
18 |
19 | const lexer = {
20 | source: str.slice()
21 | }
22 |
23 | let lineno = 1
24 | let column = 1
25 |
26 | function updatePosition (str) {
27 | const lines = str.match(/\n/g)
28 | if (lines) lineno += lines.length
29 | const i = str.lastIndexOf('\n')
30 | column = ~i ? str.length - i : column + str.length
31 | }
32 |
33 | function matcher (re) {
34 | const m = re.exec(lexer.source)
35 | if (m === null) return
36 | const str = m[0]
37 | updatePosition(str)
38 | lexer.source = lexer.source.slice(m.index + str.length)
39 | // console.log('>', arguments.callee.caller.name, "'" + m + "'")
40 | return m
41 | }
42 |
43 | lexer.data = function () {
44 | return lexer.source
45 | }
46 |
47 | lexer.pos = function () {
48 | return { column: column, lineno: lineno }
49 | }
50 |
51 | lexer.length = function () {
52 | return lexer.source.length
53 | }
54 |
55 | lexer.exec = function (re) {
56 | return re.exec(lexer.source)
57 | }
58 |
59 | lexer.peek = function (index, len) {
60 | return lexer.source.slice(index || 0, len || 1)
61 | }
62 |
63 | lexer.pop = function (index, len) {
64 | const s = lexer.source.slice(index || 0, len || 1)
65 | lexer.source = lexer.source.slice(len || 1)
66 | return s
67 | }
68 |
69 | lexer.error = function error (msg) {
70 | const err = new SyntaxError([
71 | msg, ':',
72 | lineno, ':',
73 | column
74 | ].join(''))
75 | err.reason = msg
76 | err.line = lineno
77 | err.column = column
78 | throw err
79 | }
80 |
81 | var pm = lexer.match = {}
82 |
83 | pm.whitespace = function whitespace () {
84 | const m = matcher(WHITESPACE_RE)
85 | return m && m[0]
86 | }
87 |
88 | pm.anyspace = function anyspace () {
89 | return matcher(ANYSPACE_RE)
90 | }
91 |
92 | pm.newline = function newline () {
93 | return matcher(NEWLINE_RE)
94 | }
95 |
96 | pm.string = function string () {
97 | const quote = matcher(QUOTED_RE)
98 | if (!quote) return
99 |
100 | let value = ''
101 | while (lexer.source[0] !== quote[0]) {
102 | if (lexer.length() === 0) {
103 | lexer.error('missing end of string')
104 | }
105 | value += lexer.source[0]
106 | lexer.source = lexer.source.slice(1)
107 | }
108 | lexer.source = lexer.source.slice(1)
109 | updatePosition(value)
110 | return value
111 | }
112 |
113 | pm.nonwhitespace = function nonwhitespace () {
114 | return matcher(NON_WHITESPACE_RE)
115 | }
116 |
117 | pm.comment = function comment () {
118 | const pair = lexer.peek(0, 2)
119 | let value = ''
120 |
121 | if (pair === '//') {
122 | value = lexer.pop(0, 2)
123 |
124 | while (true) {
125 | const ch = lexer.peek()
126 | if (/[\x0a\x0d]+/.test(ch)) break
127 | value += lexer.pop()
128 | }
129 |
130 | updatePosition(value)
131 | }
132 | return value
133 | }
134 |
135 | pm.parens = function parens () {
136 | let ch = lexer.peek()
137 | let value = ''
138 |
139 | if (ch === '(') {
140 | var open = 1
141 | lexer.pop()
142 |
143 | while (true) {
144 | if (lexer.length() === 0) {
145 | lexer.error('missing closing paren')
146 | }
147 |
148 | ch = lexer.peek()
149 |
150 | if (NEWLINE_RE.test(ch)) {
151 | lexer.pop()
152 | continue
153 | }
154 |
155 | if (ch === '(') {
156 | ++open
157 | } else if (ch === ')') {
158 | if (!--open) {
159 | lexer.pop()
160 | break
161 | }
162 | }
163 |
164 | value += lexer.pop(0, 1)
165 | updatePosition(value)
166 | }
167 | }
168 | return value
169 | }
170 |
171 | pm.value = function value () {
172 | let val = ''
173 | let openSingle = false
174 | let openDouble = false
175 | let openBrace = false
176 | let openBracket = false
177 | let openParen = false
178 |
179 | while (true) {
180 | if (lexer.length() === 0) {
181 | lexer.error('Unexpected end of source')
182 | }
183 |
184 | let next = lexer.peek(0, 2)
185 | let ch = lexer.pop()
186 |
187 | if (ch === '\'') openSingle = !openSingle
188 | else if (ch === '"') openDouble = !openDouble
189 | else if (ch === '{') openBrace = true
190 | else if (ch === '}') openBrace = false
191 | else if (ch === '[') openBracket = true
192 | else if (ch === ']') openBracket = false
193 | else if (ch === '(') openParen = true
194 | else if (ch === ')') openParen = false
195 |
196 | let closed = !openSingle && !openDouble &&
197 | !openBracket && !openBrace && !openParen
198 |
199 | if (closed && /[\r\n,]/.test(ch)) break
200 |
201 | // ticky bit -- if closed, after one or more whitespace, we don't
202 | // find an operator, we can asume that this is not an expression, it
203 | // must be the end of the value.
204 | if (closed && EOV_RE.test(next)) {
205 | // val += ch
206 | break
207 | }
208 | val += ch
209 | if (!lexer.length()) break
210 | }
211 | return val
212 | }
213 |
214 | pm.word = function word () {
215 | const m = matcher(WORD_RE)
216 | return m && m[0]
217 | }
218 |
219 | pm.delimiter = function delimiter () {
220 | const m = matcher(DELIMITER_RE)
221 | return m && m[0]
222 | }
223 |
224 | pm.separator = function separator () {
225 | const m = matcher(SEP_RE)
226 | return m && m[0]
227 | }
228 |
229 | pm.tagOrSymbol = function tagOrSymbol () {
230 | const m = matcher(TAGORSYMBOL_RE)
231 | return m && m[0]
232 | }
233 |
234 | pm.content = function content (preserveComments) {
235 | let value = ''
236 | let lastch = ''
237 |
238 | while (true) {
239 | let ch = lexer.peek(0, 1)
240 |
241 | if (ch === '/' && !preserveComments) {
242 | const nextch = lexer.peek(1, 2)
243 | if (lastch !== ':' && nextch === '/') {
244 | value = value.trim() // its a line comment, not a url
245 | break
246 | }
247 | } else if (NEWLINE_RE.test(ch) || lexer.length() === 0) {
248 | break
249 | }
250 | lastch = lexer.pop()
251 | value += lastch
252 | }
253 | return value
254 | }
255 | return lexer
256 | }
257 |
258 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mineral",
3 | "version": "3.0.13",
4 | "description": "A simplified fork of the jade template langauge",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "tape test/*.js",
8 | "build": "./node_modules/.bin/min ./docs/src/index.min -o ./docs"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/voltraco/mineral.git"
13 | },
14 | "author": "hij1nx",
15 | "license": "ISC",
16 | "bugs": {
17 | "url": "https://github.com/voltraco/mineral/issues"
18 | },
19 | "homepage": "https://github.com/voltraco/mineral#readme",
20 | "devDependencies": {
21 | "benchmark": "^2.1.0",
22 | "domready": "^1.0.8",
23 | "faucet": "0.0.1",
24 | "jade": "^1.11.0",
25 | "json-stringify-safe": "^5.0.1",
26 | "jstransformer-stylus": "^1.3.0",
27 | "mineral-cli": "^1.0.0",
28 | "node-chrome": "^1.1.1",
29 | "tape": "^4.6.3",
30 | "tape-run": "^2.1.3"
31 | },
32 | "dependencies": {
33 | "callsites": "^2.0.0",
34 | "he": "^1.1.0",
35 | "jstransformer": "^1.0.0",
36 | "resolve-from": "^2.0.0"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/parser.js:
--------------------------------------------------------------------------------
1 | const Lexer = require('./lexer')
2 |
3 | function parseAttributes (str) {
4 | const lexer = Lexer(str)
5 | const args = {}
6 |
7 | while (true) {
8 | lexer.match.whitespace()
9 | const name = lexer.match.word()
10 | const delimiter = lexer.match.delimiter()
11 | let value = true
12 |
13 | if (delimiter) value = lexer.match.value()
14 | if (!name) break
15 |
16 | lexer.match.whitespace()
17 | const separator = lexer.match.separator()
18 | args[name] = value
19 | // console.log('[%s] [%s] [%s] [%s]', name, delimiter, value, lexer.data())
20 | if (!lexer.length()) break
21 | }
22 | return args
23 | }
24 |
25 | module.exports = function Parser (source) {
26 | source = source.replace(/\t/g, ' ')
27 |
28 | const lexer = Lexer(source)
29 | const root = { tagOrSymbol: 'document', children: [] }
30 |
31 | let parent = root
32 | let lastIndent = 0
33 | let lastSibling = null
34 | let contentTarget = null
35 | let contentTargetIndent = 0
36 |
37 | while (lexer.length()) {
38 | const whitespace = lexer.match.whitespace()
39 |
40 | if (contentTarget) {
41 | if (whitespace.length <= contentTargetIndent) {
42 | contentTarget.content = contentTarget.content.slice(0, -1)
43 | contentTarget = null
44 | } else {
45 | // add newlines and whitespace, but trim to the current indent.
46 | const trimmed = whitespace // .slice(contentTargetIndent + 2)
47 | contentTarget.content += trimmed + lexer.match.content(true)
48 | contentTarget.content += (lexer.match.newline() || '')
49 | continue
50 | }
51 | }
52 |
53 | lexer.match.comment()
54 | const tagOrSymbol = lexer.match.tagOrSymbol()
55 |
56 | if (tagOrSymbol) {
57 | const attributes = (!contentTarget &&
58 | parseAttributes(lexer.match.parens()))
59 |
60 | lexer.match.whitespace()
61 | lexer.match.comment()
62 |
63 | const indent = whitespace.length
64 | if (indent % 2 !== 0) lexer.error('Uneven indent')
65 |
66 | const tag = {
67 | tagOrSymbol: tagOrSymbol,
68 | attributes: attributes,
69 | content: lexer.match.content(),
70 | indent: indent,
71 | children: [],
72 | pos: lexer.pos()
73 | }
74 |
75 | if (tagOrSymbol.slice(-1) === '.') {
76 | tag.tagOrSymbol = tagOrSymbol.slice(0, -1)
77 | tag.unescaped = true
78 | contentTarget = tag
79 | contentTargetIndent = indent
80 | }
81 |
82 | if (indent > lastIndent) {
83 | tag.parent = (lastSibling || parent)
84 | tag.parent.children.push(tag)
85 | lastSibling = null
86 | parent = tag
87 | } else if (indent < lastIndent) {
88 | while (parent && parent.indent >= indent) {
89 | parent = parent.parent
90 | }
91 | tag.parent = parent
92 | tag.parent.children.push(tag)
93 | lastSibling = null
94 | parent = tag
95 | } else {
96 | tag.parent = parent.parent || parent
97 | tag.parent.children.push(tag)
98 | lastSibling = tag
99 | }
100 | lastIndent = indent
101 | }
102 | lexer.match.newline()
103 | }
104 | return root
105 | }
106 |
107 |
--------------------------------------------------------------------------------
/test/browser.js:
--------------------------------------------------------------------------------
1 | const Chrome = require('node-chrome')
2 | const stringify = require('json-stringify-safe')
3 |
4 | module.exports = (source, data = {}, opts, cb) => {
5 | if (typeof opts === 'function') {
6 | cb = opts
7 | opts = {}
8 | }
9 |
10 | data = stringify(data)
11 |
12 | const chrome = Chrome(`
13 | const path = require('path')
14 | const compiler = require(path.resolve('compilers/dom'))
15 | const parser = require(path.resolve('parser'))
16 |
17 | window.onload = () => {
18 | const source = \`${source}\`
19 | const text= ${!!opts.text}
20 | const node = compiler(parser(source), ${data})
21 |
22 | if (text) {
23 | console.log(node.textContent)
24 | return window.close()
25 | }
26 |
27 | document.body.appendChild(node)
28 | console.log(document.body.innerHTML)
29 | window.close()
30 | }
31 | `)
32 |
33 | let out = ''
34 | let err = null
35 |
36 | chrome.on('stdout', d => (out += d))
37 | chrome.on('stderr', e => (err += e))
38 | chrome.on('exit', (code, sig) => cb(err, out))
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/test/cache.js:
--------------------------------------------------------------------------------
1 | const test = require('tape')
2 | const fs = require('fs')
3 | const path = require('path')
4 | const parse = require('../parser')
5 | const compile = require('../compilers/html')
6 |
7 | const base = path.join(__dirname, 'fixtures')
8 | const read = s => fs.readFileSync(path.join(base, s), 'utf8')
9 |
10 | test('a tree can be cached', assert => {
11 | const m = parse(read('./cached.min'))
12 |
13 | const template = d => compile(m, d)
14 |
15 | assert.equal(template({ foo: 100 }), '
comments
Single-line
Multi-line (`.beep` and `.boop` are commented)
note Comments have no business in compiled code, so they are removed entirely. Conditional comments are no longer supported by any IE version, so they wont be supported. If for some reason you really want comments, you can use the comment tag.