riot-compiler
Version:
Compiler for riot .tag files
1,020 lines (877 loc) • 30.8 kB
JavaScript
/**
* The riot-compiler v3.6.0
*
* @module compiler
* @version v3.6.0
* @license MIT
* @copyright Muut Inc. + contributors
*/
var sourcemap = require('./sourcemap')
var brackets = require('./brackets')
var parsers = require('./parsers')
var safeRegex = require('./safe-regex')
var jsSplitter = require('./js-splitter')
var path = require('path')
var extend = require('./parsers/_utils').mixobj
/* eslint-enable */
/**
* Source for creating regexes matching valid quoted, single-line JavaScript strings.
* It recognizes escape characters, including nested quotes and line continuation.
* @const {string}
*/
var S_LINESTR = /"[^"\n\\]*(?:\\[\S\s][^"\n\\]*)*"|'[^'\n\\]*(?:\\[\S\s][^'\n\\]*)*'/.source
/**
* Source of {@link module:brackets.S_QBLOCKS|brackets.S_QBLOCKS} for creating regexes
* matching multiline HTML strings and/or skip literal strings inside expressions.
* @const {string}
* @todo Bad thing. It recognizes escaped quotes (incorrect for HTML strings) and multiline
* strings without line continuation `'\'` (incorrect for expressions). Needs to be fixed
* ASAP but the current logic requires it to parse expressions inside attribute values :[
*/
var S_STRINGS = brackets.R_STRINGS.source
/**
* Matches pairs attribute=value, both quoted and unquoted.
* Names can contain almost all iso-8859-1 character set.
* Used by {@link module:compiler~parseAttribs|parseAttribs}, assume hidden
* expressions and compact spaces (no EOLs).
* @const {RegExp}
*/
var HTML_ATTRS = / *([-\w:\xA0-\xFF]+) ?(?:= ?('[^']*'|"[^"]*"|\S+))?/g
/**
* Matches valid HTML comments (to remove) and JS strings/regexes (to skip).
* Used by [cleanSource]{@link module:compiler~cleanSource}.
* @const {RegExp}
*/
var HTML_COMMS = RegExp(/<!--(?!>)[\S\s]*?-->/.source + '|' + S_LINESTR, 'g')
/**
* HTML_TAGS matches opening and self-closing tags, not the content.
* Used by {@link module:compiler~_compileHTML|_compileHTML} after hidding
* the expressions.
*
* 2016-01-18: exclude `'\s'` from attr capture to avoid unnecessary call to
* {@link module:compiler~parseAttribs|parseAttribs}
* @const {RegExp}
*/
var HTML_TAGS = /<(-?[A-Za-z][-\w\xA0-\xFF]*)(?:\s+([^"'/>]*(?:(?:"[^"]*"|'[^']*'|\/[^>])[^'"/>]*)*)|\s*)(\/?)>/g
/**
* Matches spaces and tabs between HTML tags
* Used by the `compact` option.
* @const RegExp
*/
var HTML_PACK = />[ \t]+<(-?[A-Za-z]|\/[-A-Za-z])/g
/**
* These attributes give error when parsed on browsers with an expression in its value.
* Ex: `<img src={ exrp_value }>`.
* Used by {@link module:compiler~parseAttribs|parseAttribs} with lowercase names only.
* @const {Array}
* @see [attributes.md](https://github.com/riot/compiler/blob/dev/doc/attributes.md)
*/
var RIOT_ATTRS = ['style', 'src', 'd', 'value']
/**
* HTML5 void elements that cannot be auto-closed.
* @const {RegExp}
* @see {@link http://www.w3.org/TR/html-markup/syntax.html#syntax-elements}
* @see {@link http://www.w3.org/TR/html5/syntax.html#void-elements}
*/
var VOID_TAGS = /^(?:input|img|br|wbr|hr|area|base|col|embed|keygen|link|meta|param|source|track)$/
/**
* Matches `<pre>` elements to hide its content ($1) from whitespace compactation.
* Used by {@link module:compiler~_compileHTML|_compileHTML} after processing the
* attributes and the self-closing tags.
* @const {RegExp}
*/
var PRE_TAGS = /<pre(?:\s+(?:[^">]*|"[^"]*")*)?>([\S\s]+?)<\/pre\s*>/gi
/**
* Matches values of the property 'type' of input elements which cause issues with
* invalid values in some browsers. These are compiled with an invalid type (an
* expression) so the browser defaults to `type="text"`. At runtime, the type is reset
* _after_ its value is replaced with the evaluated expression or an empty value.
* @const {RegExp}
*/
var SPEC_TYPES = /^"(?:number|date(?:time)?|time|month|email|color)\b/i
/**
* Matches the 'import' statement
* @const {RegExp}
*/
var IMPORT_STATEMENT = /^\s*import(?!\w|(\s)?\()(?:(?:\s|[^\s'"])*)['|"].*\n?/gm
/**
* Matches trailing spaces and tabs by line.
* @const {RegExp}
*/
var TRIM_TRAIL = /[ \t]+$/gm
var
RE_HASEXPR = safeRegex(/@#\d/, 'x01'),
RE_REPEXPR = safeRegex(/@#(\d+)/g, 'x01'),
CH_IDEXPR = '\x01#',
CH_DQCODE = '\u2057',
DQ = '"',
SQ = "'"
/**
* Normalizes eols and removes HTML comments without touching the strings,
* avoiding unnecesary replacements.
* Skip the strings is less expansive than replacing with itself.
*
* @param {string} src - The HTML source with comments
* @returns {string} HTML without comments
* @since v2.3.22
*/
function cleanSource (src) {
var
mm,
re = HTML_COMMS
if (src.indexOf('\r') !== 1) {
src = src.replace(/\r\n?/g, '\n')
}
re.lastIndex = 0
while ((mm = re.exec(src))) {
if (mm[0][0] === '<') {
src = RegExp.leftContext + RegExp.rightContext
re.lastIndex = mm[3] + 1
}
}
return src
}
/**
* Parses attributes. Force names to lowercase, enclose the values in double quotes,
* and compact spaces.
* Take care about issues in some HTML5 input elements with expressions in its value.
*
* @param {string} str - Attributes, with expressions replaced by their hash
* @param {Array} pcex - Has a `_bp` property with info about brackets
* @returns {string} Formated attributes
*/
function parseAttribs (str, pcex) {
var
list = [],
match,
type, vexp
HTML_ATTRS.lastIndex = 0
str = str.replace(/\s+/g, ' ')
while ((match = HTML_ATTRS.exec(str))) {
var
k = match[1].toLowerCase(),
v = match[2]
if (!v) {
list.push(k)
} else {
if (v[0] !== DQ) {
v = DQ + (v[0] === SQ ? v.slice(1, -1) : v) + DQ
}
if (k === 'type' && SPEC_TYPES.test(v)) {
type = v
} else {
if (RE_HASEXPR.test(v)) {
if (k === 'value') vexp = 1
if (RIOT_ATTRS.indexOf(k) !== -1) k = 'riot-' + k
}
list.push(k + '=' + v)
}
}
}
if (type) {
if (vexp) type = DQ + pcex._bp[0] + SQ + type.slice(1, -1) + SQ + pcex._bp[1] + DQ
list.push('type=' + type)
}
return list.join(' ')
}
/**
* Replaces expressions with a marker and runs expressions through the parser,
* if any, except those beginning with `"{^"` (hack for riot#1014 and riot#1090).
*
* @param {string} html - Raw html without comments
* @param {object} opts - The options, as passed to the compiler
* @param {Array} pcex - To store the extracted expressions
* @returns {string} html with its expressions replaced with markers
*
* @see {@link module:brackets.split|brackets.split}
*/
function splitHtml (html, opts, pcex) {
var _bp = pcex._bp
if (html && _bp[4].test(html)) {
var
jsfn = opts.expr && (opts.parser || opts.type) ? _compileJS : 0,
list = brackets.split(html, 0, _bp),
expr
for (var i = 1; i < list.length; i += 2) {
expr = list[i]
if (expr[0] === '^') {
expr = expr.slice(1)
} else if (jsfn) {
expr = jsfn(expr, opts).trim()
if (expr.slice(-1) === ';') expr = expr.slice(0, -1)
}
list[i] = CH_IDEXPR + (pcex.push(expr) - 1) + _bp[1]
}
html = list.join('')
}
return html
}
/**
* Cleans and restores hidden expressions encoding double quotes to prevent issues
* with browsers (breaks attributes) and encode `"<>"` for expressions with raw HTML.
*
* @param {string} html - The HTML source with hidden expresions
* @param {Array} pcex - Array with unformatted expressions
* @returns {string} HTML with clean expressions in its place.
*/
function restoreExpr (html, pcex) {
if (pcex.length) {
html = html.replace(RE_REPEXPR, function (_, d) {
return pcex._bp[0] + pcex[d].trim().replace(/[\r\n]+/g, ' ').replace(/"/g, CH_DQCODE)
})
}
return html
}
/**
* The internal HTML compiler.
*
* @param {string} html - Raw HTML string
* @param {object} opts - Compilation options received by compile or compileHTML
* @param {Array} pcex - To store extracted expressions, must include `_bp`
* @returns {string} Parsed HTML code which can be used by `riot.tag2`.
*
* @see {@link http://www.w3.org/TR/html5/syntax.html}
*/
function _compileHTML (html, opts, pcex) {
if (!/\S/.test(html)) return ''
html = splitHtml(html, opts, pcex)
.replace(HTML_TAGS, function (_, name, attr, ends) {
name = name.toLowerCase()
ends = ends && !VOID_TAGS.test(name) ? '></' + name : ''
if (attr) name += ' ' + parseAttribs(attr, pcex)
return '<' + name + ends + '>'
})
if (!opts.whitespace) {
var p = []
if (/<pre[\s>]/.test(html)) {
html = html.replace(PRE_TAGS, function (q) {
p.push(q)
return '\u0002'
})
}
html = html.trim().replace(/\s+/g, ' ')
if (p.length) html = html.replace(/\u0002/g, function () { return p.shift() }) // eslint-disable-line
}
if (opts.compact) html = html.replace(HTML_PACK, '><$1')
return restoreExpr(html, pcex).replace(TRIM_TRAIL, '')
}
/**
* Public interface to the internal HTML compiler, parses and formats the HTML part.
*
* - Runs each expression through the parser and replace it with a marker
* - Removes trailing tab and spaces
* - Normalizes and formats the attribute-value pairs
* - Closes self-closing tags
* - Normalizes and restores the expressions
*
* @param {string} html - Can contain embedded HTML comments and literal whitespace
* @param {Object} [opts] - User options.
* @param {Array} [pcex] - To store precompiled expressions
* @returns {string} The parsed HTML markup, which can be used by `riot.tag2`
* @static
*/
function compileHTML (html, opts, pcex) {
if (Array.isArray(opts)) {
pcex = opts
opts = {}
} else {
if (!pcex) pcex = []
if (!opts) opts = {}
}
pcex._bp = brackets.array(opts.brackets)
return _compileHTML(cleanSource(html), opts, pcex)
}
/**
* Matches ES6 methods across multiple lines up to its first curly brace.
* Used by the {@link module:compiler~riotjs|riotjs} parser.
*
* 2016-01-18: rewritten to capture only the method name (performant)
* @const {RegExp}
*/
var JS_ES6SIGN = /^[ \t]*(((?:async|\*)\s*)?([$_A-Za-z][$\w]*))\s*\([^()]*\)\s*{/m
/**
* Default parser for JavaScript, supports ES6-like method syntax
*
* @param {string} js - Raw JavaScript code
* @returns {string} Code with ES6 methods converted to ES5, comments removed.
*/
function riotjs (js) {
var
parts = [],
match,
toes5,
pos,
method,
prefix,
name,
RE = RegExp
const src = jsSplitter(js)
js = src.shift().join('<%>')
while ((match = js.match(JS_ES6SIGN))) {
parts.push(RE.leftContext)
js = RE.rightContext
pos = skipBody(js)
method = match[1]
prefix = match[2] || ''
name = match[3]
toes5 = !/^(?:if|while|for|switch|catch|function)$/.test(name)
if (toes5) {
name = match[0].replace(method, 'this.' + name + ' =' + prefix + ' function')
} else {
name = match[0]
}
parts.push(name, js.slice(0, pos))
js = js.slice(pos)
if (toes5 && !/^\s*.\s*bind\b/.test(js)) parts.push('.bind(this)')
}
if (parts.length) {
js = parts.join('') + js
}
if (src.length) {
js = js.replace(/<%>/g, function () {
return src.shift()
})
}
return js
function skipBody (s) {
var r = /[{}]/g
var i = 1
while (i && r.exec(s)) {
if (s[r.lastIndex - 1] === '{') ++i
else --i
}
return i ? s.length : r.lastIndex
}
}
/**
* Internal JavaScript compilation.
*
* @param {string} js - Raw JavaScript code
* @param {object} opts - Compiler options
* @param {string} type - Parser name, one of {@link module:parsers.js|parsers.js}
* @param {object} parserOpts - User options passed to the parser
* @param {string} url - Of the file being compiled, passed to the parser
* @returns {string} Compiled code, eols normalized, trailing spaces removed
*
* @throws Will throw "JS parser not found" if the JS parser cannot be loaded.
* @see {@link module:compiler.compileJS|compileJS}
*/
function _compileJS (js, opts, type, parserOpts, url) {
if (!/\S/.test(js)) return ''
if (!type) type = opts.type
var parser = opts.parser || type && parsers._req('js.' + type, true) || riotjs
return parser(js, parserOpts, url).replace(/\r\n?/g, '\n').replace(TRIM_TRAIL, '')
}
/**
* Public interface to the internal JavaScript compiler, runs the parser with
* the JavaScript code, defaults to `riotjs`.
*
* - If the given code is empty or whitespaces only, returns an empty string
* - Determines the parser to use, by default the internal riotjs function
* - Call the parser, the default {@link module:compiler~riotjs|riotjs} removes comments,
* converts ES6 method signatures to ES5 and bind to `this` if neccesary
* - Normalizes line-endings and trims trailing spaces before return the result
*
* @param {string} js - Buffer with the javascript code
* @param {Object} [opts] - Compiler options (DEPRECATED parameter, don't use it)
* @param {string} [type=riotjs] - Parser name, one of {@link module:parsers.js|parsers.js}
* @param {Object} [userOpts={}] - User options
* @param {string} [userOpts.url=process.cwd] - Url of the .tag file (passed to the parser)
* @param {object} [userOpts.parserOpts={}] - User options (passed to the parser)
* @returns {string} Parsed JavaScript, eols normalized, trailing spaces removed
* @static
*
* @see {@link module:compiler~_compileJS|_compileJS}
*/
function compileJS (js, opts, type, userOpts) {
if (typeof opts === 'string') {
userOpts = type
type = opts
opts = {}
}
if (type && typeof type === 'object') {
userOpts = type
type = ''
}
if (!userOpts) userOpts = {}
return _compileJS(js, opts || {}, type, userOpts.parserOptions, userOpts.url)
}
var CSS_SELECTOR = RegExp('([{}]|^)[; ]*((?:[^@ ;{}][^{}]*)?[^@ ;{}:] ?)(?={)|' + S_LINESTR, 'g')
/**
* Parses styles enclosed in a "scoped" tag (`scoped` was removed from HTML5).
* The "css" string is received without comments or surrounding spaces.
*
* @param {string} tag - Tag name of the root element
* @param {string} css - The CSS code
* @returns {string} CSS with the styles scoped to the root element
*/
function scopedCSS (tag, css) {
var scope = ':scope'
var selectorsBlacklist = ['from', 'to', ':host']
return css.replace(CSS_SELECTOR, function (m, p1, p2) {
if (!p2) return m
p2 = p2.replace(/[^,]+/g, function (sel) {
var s = sel.trim()
if (s.indexOf(tag) === 0) {
return sel
}
if (!s || selectorsBlacklist.indexOf(s) > -1 || s.slice(-1) === '%') {
return sel
}
if (s.indexOf(scope) < 0) {
s = tag + ' ' + s + ',[data-is="' + tag + '"] ' + s
} else {
s = s.replace(scope, tag) + ',' +
s.replace(scope, '[data-is="' + tag + '"]')
}
return s
})
return p1 ? p1 + ' ' + p2 : p2
})
}
/**
* Internal CSS compilation.
* Runs any parser for style blocks and calls scopedCSS if required.
*
* @param {string} css - Raw CSS
* @param {string} tag - Tag name to which the style belongs to
* @param {string} [type=css] - Parser name to run
* @param {object} [opts={}] - User options
* @returns {string} The compiled style, whitespace compacted and trimmed
*
* @throws Will throw "CSS parser not found" if the CSS parser cannot be loaded.
* @throws Using the _scoped_ option with no tagName will throw an error.
* @see {@link module:compiler.compileCSS|compileCSS}
*/
function _compileCSS (css, tag, type, opts) {
opts = opts || {}
if (type) {
if (type !== 'css') {
var parser = parsers._req('css.' + type, true)
css = parser(tag, css, opts.parserOpts || {}, opts.url)
}
}
css = css.replace(brackets.R_MLCOMMS, '').replace(/\s+/g, ' ').trim()
if (tag && (!opts.parserOpts || opts.parserOpts.prefixCSS))
css = scopedCSS(tag, css)
return css
}
/**
* Public API, wrapper of the internal {@link module:compiler~_compileCSS|_compileCSS}
* function, rearranges its parameters and makes these can be omitted.
*
* - If the given code is empty or whitespaces only, returns an empty string
* - Determines the parser to use, none by default
* - Call the parser, if any
* - Normalizes line-endings and trims trailing spaces
* - Call the {@link module:compiler~scopedCSS|scopedCSS} function if required by the
* parameter _opts_
*
* @param {string} css - Raw style block
* @param {string} [type] - Parser name, one of {@link module:parsers.css|parsers.css}
* @param {object} [opts] - User options
* @param {boolean} [opts.scoped] - Convert to Scoped CSS (requires _tagName_)
* @param {string} [opts.tagName] - Name of the root tag owner of the styles
* @param {string} [opts.url=process.cwd] - Url of the .tag file
* @param {object} [opts.parserOpts={}] - Options for the parser
* @returns {string} The processed style block
* @static
*
* @see {@link module:compiler~_compileCSS|_compileCSS}
*/
function compileCSS (css, type, opts) {
if (type && typeof type === 'object') {
opts = type
type = ''
} else if (!opts) opts = {}
return _compileCSS(css, opts.tagName, type, opts)
}
/**
* The "defer" attribute is used to ignore `script` elements (useful for SSR).
*
* This regex is used by {@link module:compiler~getCode|getCode} to check and by
* {@link module:compiler.compile|compile} to remove the keyword `defer` from
* `<script>` tags.
* @const {RegExp}
*/
var DEFER_ATTR = /\sdefer(?=\s|>|$)/i
/**
* Matches attributes 'type=value', for `<script>` and `<style>` tags.
* This regex does not expect expressions nor escaped quotes.
* @const {RegExp}
*/
var TYPE_ATTR = /\stype\s*=\s*(?:(['"])(.+?)\1|(\S+))/i
/**
* Source string for creating generic regexes matching pairs of `attribute=value`. Used
* by {@link module:compiler~getAttrib|getAttrib} for the `option`, `src`, and `charset`
* attributes, handles escaped quotes and unquoted JSON objects with no nested brackets.
* @const {string}
*/
var MISC_ATTR = '\\s*=\\s*(' + S_STRINGS + '|{[^}]+}|\\S+)'
/**
* Matches the last HTML tag ending a line. This can be one of:
* - self-closing tag
* - closing tag
* - tag without attributes
* - void tag with (optional) attributes
*
* Be aware that this regex still can be fooled by strange code like:
* ```js
* x <y -y>
* z
* ```
* @const {RegExp}
*/
var END_TAGS = /\/>\n|^<(?:\/?-?[A-Za-z][-\w\xA0-\xFF]*\s*|-?[A-Za-z][-\w\xA0-\xFF]*\s+[-\w:\xA0-\xFF][\S\s]*?)>\n/
/**
* Encloses the given string in single quotes.
*
* 2016-01-18: we must escape single quotes and backslashes before quoting the
* string, but there's no need to care about line-endings unless is required,
* as each submodule normalizes the lines.
*
* @param {string} s - The unquoted, source string
* @param {number} r - If 1, escape embeded EOLs in the source
* @returns {string} Quoted string, with escaped single-quotes and backslashes.
*/
function _q (s, r) {
if (!s) return "''"
s = SQ + s.replace(/\\/g, '\\\\').replace(/'/g, "\\'") + SQ
return r && s.indexOf('\n') !== -1 ? s.replace(/\n/g, '\\n') : s
}
/**
* Generates code to call the `riot.tag2` function with the processed parts.
*
* @param {string} name - The tag name
* @param {string} html - HTML (can contain embeded eols)
* @param {string} css - Styles
* @param {string} attr - Root attributes
* @param {string} js - JavaScript "constructor"
* @param {string} imports - Code containing 'import' statements
* @param {object} opts - Compiler options
* @returns {string} Code to call `riot.tag2`
*/
function mktag (name, html, css, attr, js, imports, opts) {
var
c = opts.debug ? ',\n ' : ', ',
s = '});'
if (js && js.slice(-1) !== '\n') s = '\n' + s
return imports + 'riot.tag2(\'' + name + SQ +
c + _q(html, 1) +
c + _q(css) +
c + _q(attr) + ', function(opts) {\n' + js + s
}
/**
* Used by the main {@link module:compiler.compile|compile} function, separates the
* HTML and JS parts of the tag. The last HTML element (can be a `<script>` block) must
* terminate a line.
*
* @param {string} str - Tag content, normalized, without attributes
* @returns {Array} Parts: `[HTML, JavaScript]`
*/
function splitBlocks (str) {
if (/<[-\w]/.test(str)) {
var
m,
k = str.lastIndexOf('<'),
n = str.length
while (k !== -1) {
m = str.slice(k, n).match(END_TAGS)
if (m) {
k += m.index + m[0].length
m = str.slice(0, k)
if (m.slice(-5) === '<-/>\n') m = m.slice(0, -5)
return [m, str.slice(k)]
}
n = k
k = str.lastIndexOf('<', k - 1)
}
}
return ['', str]
}
/**
* Returns the value of the 'type' attribute, with the prefix "text/" removed.
*
* @param {string} attribs - The attributes list
* @returns {string} Attribute value, defaults to empty string
*/
function getType (attribs) {
if (attribs) {
var match = attribs.match(TYPE_ATTR)
match = match && (match[2] || match[3])
if (match) {
return match.replace('text/', '')
}
}
return ''
}
/**
* Returns the value of any attribute, or the empty string for missing attribute.
*
* @param {string} attribs - The attribute list
* @param {string} name - Attribute name
* @returns {string} Attribute value, defaults to empty string
*/
function getAttrib (attribs, name) {
if (attribs) {
var match = attribs.match(RegExp('\\s' + name + MISC_ATTR, 'i'))
match = match && match[1]
if (match) {
return (/^['"]/).test(match) ? match.slice(1, -1) : match
}
}
return ''
}
/**
* Unescape any html string
* @param {string} str escaped html string
* @returns {string} unescaped html string
*/
function unescapeHTML (str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, '\'')
}
/**
* Gets the parser options from the "options" attribute.
*
* @param {string} attribs - The attribute list
* @returns {object} Parsed options, or null if no options
*/
function getParserOptions (attribs) {
var opts = unescapeHTML(getAttrib(attribs, 'options'))
return opts ? JSON.parse(opts) : null
}
/**
* Gets the parsed code for the received JavaScript code.
* The node version can read the code from the file system. The filename is
* specified through the _src_ attribute and can be absolute or relative to _base_.
*
* @param {string} code - The unprocessed JavaScript code
* @param {object} opts - Compiler options
* @param {string} attribs - Attribute list
* @param {string} base - Full filename or path of the file being processed
* @returns {string} Parsed code
*/
function getCode (code, opts, attribs, base) {
var
type = getType(attribs),
src = getAttrib(attribs, 'src'),
jsParserOptions = extend({}, opts.parserOptions.js)
if (src) {
if (DEFER_ATTR.test(attribs)) return false
var charset = getAttrib(attribs, 'charset'),
file = path.resolve(path.dirname(base), src)
code = require('fs').readFileSync(file, charset || 'utf8')
}
return _compileJS(
code,
opts,
type,
extend(jsParserOptions, getParserOptions(attribs)),
base
)
}
/**
* Gets the parsed styles for the received CSS code.
*
* @param {string} code - Unprocessed CSS
* @param {object} opts - Compiler options
* @param {string} attribs - Attribute list
* @param {string} url - Of the file being processed
* @param {string} tag - Tag name of the root element
* @returns {string} Parsed styles
*/
function cssCode (code, opts, attribs, url, tag) {
var
parserStyleOptions = extend({}, opts.parserOptions.style),
extraOpts = {
parserOpts: extend(parserStyleOptions, getParserOptions(attribs)),
url: url
}
return _compileCSS(code, tag, getType(attribs) || opts.style, extraOpts)
}
/**
* Runs the external HTML parser for the entire tag file
*
* @param {string} html - Entire, untouched html received for the compiler
* @param {string} url - The source url or file name
* @param {string} lang - Parser's name, one of {@link module:parsers.html|parsers.html}
* @param {object} opts - Extra option passed to the parser
* @returns {string} parsed html
*
* @throws Will throw "Template parser not found" if the HTML parser cannot be loaded.
*/
function compileTemplate (html, url, lang, opts) {
var parser = parsers._req('html.' + lang, true)
return parser(html, opts, url)
}
var
/**
* Matches HTML elements. The opening and closing tags of multiline elements must have
* the same indentation (size and type).
*
* `CUST_TAG` recognizes escaped quotes, allowing its insertion into JS strings inside
* unquoted expressions, but disallows the character '>' within unquoted attribute values.
* @const {RegExp}
*/
CUST_TAG = RegExp(/^([ \t]*)<(-?[A-Za-z][-\w\xA0-\xFF]*)(?:\s+([^'"/>]+(?:(?:@|\/[^>])[^'"/>]*)*)|\s*)?(?:\/>|>[ \t]*\n?([\S\s]*)^\1<\/\2\s*>|>(.*)<\/\2\s*>)/
.source.replace('@', S_STRINGS), 'gim'),
/**
* Matches `script` elements, capturing its attributes in $1 and its content in $2.
* Disallows the character '>' inside quoted or unquoted attribute values.
* @const {RegExp}
*/
SCRIPTS = /<script(\s+[^>]*)?>\n?([\S\s]*?)<\/script\s*>/gi,
/**
* Matches `style` elements, capturing its attributes in $1 and its content in $2.
* Disallows the character '>' inside quoted or unquoted attribute values.
* @const {RegExp}
*/
STYLES = /<style(\s+[^>]*)?>\n?([\S\s]*?)<\/style\s*>/gi
/**
* The main compiler processes all custom tags, one by one.
*
* - Sends the received source to the html parser, if any is specified
* - Normalizes eols, removes HTML comments and trim trailing spaces
* - Searches the HTML elements and extract: tag name, root attributes, and content
* - Parses the root attributes. Found expressions are stored in `pcex[]`
* - Removes _HTML_ comments and trims trailing whitespace from the content
* - For one-line tags, process all the content as HTML
* - For multiline tags, separates the HTML from any untagged JS block and, from
* the html, extract and process the `style` and `script` elements
* - Parses the remaining html, found expressions are added to `pcex[]`
* - Parses the untagged JavaScript block, if any
* - If the `entities` option was received, returns an object with the parts,
* if not, returns the code neccesary to call the `riot.tag2` function to
* create a Tag instance at runtime.
*
* In .tag files, a custom tag can span multiple lines, but there should be no other
* elements at the start of the line (comments inclusive). Custom tags in html files
* don't have this restriction.
*
* @param {string} src - String with zero or more custom riot tags
* @param {Object} [opts={}] - User options
* @param {string} [url=./.] - Filename or url of the file being processed
* @returns {string} JavaScript code to build a Tag by the `riot.tag2` function
* @static
*/
function compile (src, opts, url) {
var
parts = [],
included,
output = src,
defaultParserptions = {
template: {},
js: {},
style: {
prefixCSS: true
}
}
if (!opts) opts = {}
opts.parserOptions = extend(defaultParserptions, opts.parserOptions || {})
included = opts.exclude
? function (s) { return opts.exclude.indexOf(s) < 0 } : function () { return 1 }
if (!url) url = process.cwd() + '/.'
var _bp = brackets.array(opts.brackets)
if (opts.template) {
output = compileTemplate(output, url, opts.template, opts.parserOptions.template)
}
output = cleanSource(output)
.replace(CUST_TAG, function (_, indent, tagName, attribs, body, body2) {
var
jscode = '',
styles = '',
html = '',
imports = '',
pcex = []
pcex._bp = _bp
tagName = tagName.toLowerCase()
attribs = attribs && included('attribs')
? restoreExpr(
parseAttribs(
splitHtml(attribs, opts, pcex),
pcex),
pcex) : ''
if ((body || (body = body2)) && /\S/.test(body)) {
if (body2) {
if (included('html')) html = _compileHTML(body2, opts, pcex)
} else {
body = body.replace(RegExp('^' + indent, 'gm'), '')
body = body.replace(SCRIPTS, function (_m, _attrs, _script) {
if (included('js')) {
var code = getCode(_script, opts, _attrs, url)
if (code === false) return _m.replace(DEFER_ATTR, '')
if (code) jscode += (jscode ? '\n' : '') + code
}
return ''
})
body = body.replace(STYLES, function (_m, _attrs, _style) {
if (included('css')) {
styles += (styles ? ' ' : '') + cssCode(_style, opts, _attrs, url, tagName)
}
return ''
})
var blocks = splitBlocks(body.replace(TRIM_TRAIL, ''))
if (included('html')) {
html = _compileHTML(blocks[0], opts, pcex)
}
if (included('js')) {
body = _compileJS(blocks[1], opts, null, null, url)
if (body) jscode += (jscode ? '\n' : '') + body
jscode = jscode.replace(IMPORT_STATEMENT, function (s) {
imports += s.trim() + '\n'
return ''
})
}
}
}
jscode = /\S/.test(jscode) ? jscode.replace(/\n{3,}/g, '\n\n') : ''
if (opts.entities) {
parts.push({
tagName: tagName,
html: html,
css: styles,
attribs: attribs,
js: jscode,
imports: imports
})
return ''
}
return mktag(tagName, html, styles, attribs, jscode, imports, opts)
})
if (opts.entities) return parts
if (opts.debug && url.slice(-2) !== '/.') {
if (/^[\\/]/.test(url)) url = path.relative('.', url)
output = '//src: ' + url.replace(/\\/g, '/') + '\n' + output
}
if (opts.sourcemap) {
var map = sourcemap({
source: src,
generated: output,
file: url
})
if (opts.sourcemap === 'inline') {
output += '\n' + sourcemap.toInlineComment(map)
return output
}
return {
map: map,
code: output
}
}
return output
}
module.exports = {
compile: compile,
html: compileHTML,
style: _compileCSS,
css: compileCSS,
js: compileJS,
parsers: parsers,
version: 'v3.6.0'
}