UNPKG

dumbymap

Version:

Generate interactive maps from Semantic HTML

238 lines (207 loc) 6.49 kB
/** * 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 = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', } 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 }