dumbymap
Version:
Generate interactive maps from Semantic HTML
238 lines (207 loc) • 6.49 kB
JavaScript
/**
* parse {.class #id key=val} strings
*/
export function getAttrs (str, start, options) {
// not tab, line feed, form feed, space, solidus, greater than sign, quotation mark, apostrophe and equals sign
const allowedKeyChars = /[^\t\n\f />"'=]/
const pairSeparator = ' '
const keySeparator = '='
const classChar = '.'
const idChar = '#'
const attrs = []
let key = ''
let value = ''
let parsingKey = true
let valueInsideQuotes = false
// read inside {}
// start + left delimiter length to avoid beginning {
// breaks when } is found or end of string
for (let i = start + options.leftDelimiter.length; i < str.length; i++) {
if (str.slice(i, i + options.rightDelimiter.length) === options.rightDelimiter) {
if (key !== '') { attrs.push([key, value]) }
break
}
const char_ = str.charAt(i)
// switch to reading value if equal sign
if (char_ === keySeparator && parsingKey) {
parsingKey = false
continue
}
// {.class} {..css-module}
if (char_ === classChar && key === '') {
if (str.charAt(i + 1) === classChar) {
key = 'css-module'
i += 1
} else {
key = 'class'
}
parsingKey = false
continue
}
// {#id}
if (char_ === idChar && key === '') {
key = 'id'
parsingKey = false
continue
}
// {value="inside quotes"}
if (char_ === '"' && value === '' && !valueInsideQuotes) {
valueInsideQuotes = true
continue
}
if (char_ === '"' && valueInsideQuotes) {
valueInsideQuotes = false
continue
}
// read next key/value pair
if ((char_ === pairSeparator && !valueInsideQuotes)) {
if (key === '') {
// beginning or ending space: { .red } vs {.red}
continue
}
attrs.push([key, value])
key = ''
value = ''
parsingKey = true
continue
}
// continue if character not allowed
if (parsingKey && char_.search(allowedKeyChars) === -1) {
continue
}
// no other conditions met; append to key/value
if (parsingKey) {
key += char_
continue
}
value += char_
}
if (options.allowedAttributes && options.allowedAttributes.length) {
const allowedAttributes = options.allowedAttributes
return attrs.filter(function (attrPair) {
const attr = attrPair[0]
function isAllowedAttribute (allowedAttribute) {
return (attr === allowedAttribute ||
(allowedAttribute instanceof RegExp && allowedAttribute.test(attr))
)
}
return allowedAttributes.some(isAllowedAttribute)
})
}
return attrs
}
/**
* add attributes from [['key', 'val']] list
*/
export function addAttrs (attrs, token) {
for (let j = 0, l = attrs.length; j < l; ++j) {
const key = attrs[j][0]
if (key === 'class') {
token.attrJoin('class', attrs[j][1])
} else if (key === 'css-module') {
token.attrJoin('css-module', attrs[j][1])
} else {
token.attrPush(attrs[j])
}
}
return token
}
/**
* Does string have properly formatted curly?
*/
export function hasDelimiters (where, options) {
if (!where) {
throw new Error('Parameter `where` not passed. Should be "start", "end" or "only".')
}
return function (str) {
const minCurlyLength = options.leftDelimiter.length + 1 + options.rightDelimiter.length
if (!str || typeof str !== 'string' || str.length < minCurlyLength) {
return false
}
function validCurlyLength (curly) {
const isClass = curly.charAt(options.leftDelimiter.length) === '.'
const isId = curly.charAt(options.leftDelimiter.length) === '#'
return (isClass || isId)
? curly.length >= (minCurlyLength + 1)
: curly.length >= minCurlyLength
}
let start, end, slice, nextChar
const rightDelimiterMinimumShift = minCurlyLength - options.rightDelimiter.length
switch (where) {
case 'start':
slice = str.slice(0, options.leftDelimiter.length)
start = slice === options.leftDelimiter ? 0 : -1
end = start === -1 ? -1 : str.indexOf(options.rightDelimiter, rightDelimiterMinimumShift)
nextChar = str.charAt(end + options.rightDelimiter.length)
if (nextChar && options.rightDelimiter.indexOf(nextChar) !== -1) {
end = -1
}
break
case 'end':
start = str.lastIndexOf(options.leftDelimiter)
end = start === -1 ? -1 : str.indexOf(options.rightDelimiter, start + rightDelimiterMinimumShift)
end = end === str.length - options.rightDelimiter.length ? end : -1
break
case 'only':
slice = str.slice(0, options.leftDelimiter.length)
start = slice === options.leftDelimiter ? 0 : -1
slice = str.slice(str.length - options.rightDelimiter.length)
end = slice === options.rightDelimiter ? str.length - options.rightDelimiter.length : -1
break
default:
throw new Error(`Unexpected case ${where}, expected 'start', 'end' or 'only'`)
}
return start !== -1 && end !== -1 && validCurlyLength(str.substring(start, end + options.rightDelimiter.length))
}
}
/**
* Removes last curly from string.
*/
export function removeDelimiter (str, options) {
const start = escapeRegExp(options.leftDelimiter)
const end = escapeRegExp(options.rightDelimiter)
const curly = new RegExp(
'[ \\n]?' + start + '[^' + start + end + ']+' + end + '$',
)
const pos = str.search(curly)
return pos !== -1 ? str.slice(0, pos) : str
}
export function escapeRegExp (s) {
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
}
/**
* get last element of array
*/
export function getMatchingOpeningToken (tokens, i) {
if (tokens[i].type === 'softbreak') {
return false
}
if (tokens[i].nesting === 0) {
return tokens[i]
}
const level = tokens[i].level
const type = tokens[i].type.replace('_close', '_open')
for (; i >= 0; --i) {
if (tokens[i].type === type && tokens[i].level === level) {
return tokens[i]
}
}
return false
}
const HTML_ESCAPE_TEST_RE = /[&<>"]/
const HTML_ESCAPE_REPLACE_RE = /[&<>"]/g
const HTML_REPLACEMENTS = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
}
function replaceUnsafeChar (ch) {
return HTML_REPLACEMENTS[ch]
}
export function escapeHtml (str) {
if (HTML_ESCAPE_TEST_RE.test(str)) {
return str.replace(HTML_ESCAPE_REPLACE_RE, replaceUnsafeChar)
}
return str
}