Files
penracourses/static/tagulous/tagulous.js
T
Ross 3367328d94 .
2021-07-26 10:48:08 +01:00

249 lines
8.0 KiB
JavaScript

/*****************************************************************************
** Common tagulous functionality
*****************************************************************************/
var Tagulous = (function () {
/**************************************************************************
** Port of utils.py
*/
// Constants to improve legibility
var COMMA = ',',
SPACE = ' ',
QUOTE = '"'
;
function escapeQuotes(count) {
return Array(Math.floor(count / 2) + 1).join(QUOTE);
}
function parseTags(str, spaceDelimiter, withRaw) {
/** Parse a tag string
Returns a sorted list of unique tags
If spaceDelimiter is false, only commas will be used as the tag
name delimiter. If it is unset, it will default to true; when true,
spaces are used as well as commas.
If withRaw == true, returns a tuple of two lists:
tags List of tags, unsorted and not unique
raws List of raw strings after parsed tag
If the last tag is not explicitly ended with a
delimiter, the corresponding item in ``raws `` will be
``null`` to indicate that the parser unexpectedly ran
out of characters.
This can help with parsing live input; if the last item
in ``raw`` is an empty string the tag has been closed;
if it is ``null`` then the tag is still being written.
*/
// Empty string is easy
if (!str) {
if (withRaw) {
return [[],[]];
}
return [];
}
// Prep vars for parser
var tags = [],
raws = [],
tag = '',
delimiter = SPACE,
strLen = str.length,
strLast = strLen - 1,
index, inQuote, chr, tagLen, leftCount, rightCount
;
// Disable spaces
if (spaceDelimiter === false) {
delimiter = COMMA;
}
// Loop through chars
for (index=0; index<strLen; index++) {
chr = str[index];
// See if it's a delimiter
if (!inQuote) {
// Comma delimiter takes priority
if (delimiter !== COMMA && chr === COMMA) {
delimiter = COMMA;
// All previous tags were actually just one tag
tag = str.substring(0, index).replace(/^ +| +$/g, '');
tags = [];
raws = [];
// Strip start/end quotes
tagLen = tag.length;
tag = tag.replace(/^"+/, '');
leftCount = tagLen - tag.length;
tagLen = tag.length;
tag = tag.replace(/"+$/, '');
rightCount = tagLen - tag.length;
// Escape inner quotes
tag = tag.replace(/""/g, QUOTE);
// Add back escaped start/end quotes
tag = escapeQuotes(leftCount) +
tag +
escapeQuotes(rightCount)
;
// Add back insignificant unquoted quotes
if (leftCount % 2 == 1) {
if (rightCount % 2 != 1) {
tag = QUOTE + tag;
}
} else if (rightCount % 2 == 1) {
tag += QUOTE;
}
}
// Found end of tag
if (chr === delimiter) {
tag = tag.replace(/ +$/, '');
if (tag) {
tags.push(tag);
if (withRaw) {
raws.push(str.substring(index + 1));
}
tag = '';
}
continue;
}
// If tag is empty, ignore whitespace
if (!tag && chr === SPACE) {
continue;
}
}
// Now either in a quote, or not a delimiter
// If it's not a quote, add to tag
if (chr != QUOTE) {
tag += chr;
continue;
}
// Char is quote - count how many quotes appear here and skip them
leftCount = 1;
while (index < strLast && str[index + 1] == QUOTE) {
leftCount++;
index++;
}
if (!tag) {
// Quote at start
// If an odd number, now in quote
if (leftCount % 2 == 1) {
inQuote = true;
}
// Tag starts with escaped quotes
tag = escapeQuotes(leftCount);
} else {
// Quote in middle or at end
// Add any escaped
tag += escapeQuotes(leftCount);
// An odd number followed by a delimiter will mean it has ended
// Need to look ahead to figure it out
if (leftCount % 2 == 1) {
// If it's the last character, it has closed
if (index == strLast) {
inQuote = false;
break;
}
for (var i2=index + 1, c2; i2<strLen; i2++) {
c2 = str[i2];
if (c2 == SPACE) {
if (delimiter == SPACE) {
// Quotes closed; tag will end next loop
inQuote = false;
break;
} else {
// Spaces are insignificant during whitespace
// Tag may continue, keep checking chars
continue;
}
} else if (c2 == COMMA) {
// Quotes closed; tag will end next loop
// Delimiter doesn't matter, comma always wins
inQuote = false;
break;
}
// Tag has not ended
// Add odd quote to tag and keep building
tag += QUOTE;
break;
}
}
}
}
// Chars expanded
if (tag) {
// Partial tag remains; add to stack
if (inQuote) {
// Add the quote back to the start - it wasn't significant
tag = QUOTE + tag;
}
tags.push(tag);
if (withRaw) {
// Indicate that the ending was unexpected
raws.push(null);
}
}
// If withRaw is true, don't want to filter or sort tags
if (withRaw) {
return [tags, raws];
}
// Enforce uniqueness and sort
for (var unique={}, finalTags=[], i=0, l=tags.length; i<l; i++) {
if (!unique[tags[i]]) {
finalTags.push(tags[i]);
unique[tags[i]] = true;
}
}
finalTags.sort();
return finalTags;
}
function renderTags(tags) {
var safe = [], i, str;
for (i=0; i<tags.length; i++) {
str = tags[i].replace(/"/g, QUOTE + QUOTE);
if (!str) {
continue;
}
if (str.indexOf(COMMA) > -1 || str.indexOf(SPACE) > -1) {
safe.push(QUOTE + str + QUOTE);
} else {
safe.push(str);
}
}
safe.sort();
return safe.join(', ');
}
return {
parseTags: parseTags,
renderTags: renderTags
};
})();