'use strict';
var nodeHtmlParser = require('node-html-parser');
/*
* Copyright (c) 2022.
* Author Peter Placzek (tada5hi)
* For the full copyright and license information,
* view the LICENSE file that was distributed with this source code.
*/ exports.QuoteType = void 0;
(function(QuoteType) {
/**
* Always quote the attribute value
*/ QuoteType["always"] = "always";
/**
* Never quote the attributes value
*/ QuoteType["never"] = "never";
/**
* Only quote the attributes value when it contains spaces to equals
*/ QuoteType["auto"] = "auto";
})(exports.QuoteType || (exports.QuoteType = {}));
/*
* Copyright (c) 2022.
* Author Peter Placzek (tada5hi)
* For the full copyright and license information,
* view the LICENSE file that was distributed with this source code.
*/ /**
* Convert 10base to 16base.
*
* @param input
*/ function toHex(input) {
let number = parseInt(`${input}`, 10);
if (Number.isNaN(number)) {
return '00';
}
number = Math.max(0, Math.min(number, 255)).toString(16);
return number.length < 2 ? `0${number}` : number;
}
/**
* Convert rgb, rgba to hex code.
*
* @param input
*/ function normaliseColor(input) {
input = input || '#000';
let match = input.match(/rgb\((\d{1,3}),\s*?(\d{1,3}),\s*?(\d{1,3})\)/i);
// rgb(n,n,n);
if (match) {
return `#${toHex(match[1])}${toHex(match[2])}${toHex(match[3])}`;
}
match = input.match(/#([0-f])([0-f])([0-f])\s*?$/i);
// expand shorthand
if (match) {
return `#${match[1]}${match[1]}${match[2]}${match[2]}${match[3]}${match[3]}`;
}
return input;
}
/*
* Copyright (c) 2022.
* Author Peter Placzek (tada5hi)
* For the full copyright and license information,
* view the LICENSE file that was distributed with this source code.
*/ function stripQuotes(str) {
return str ? str.replace(/\\(.)/g, '$1').replace(/^(["'])(.*?)\1$/, '$2') : str;
}
/*
* Copyright (c) 2022-2022.
* Author Peter Placzek (tada5hi)
* For the full copyright and license information,
* view the LICENSE file that was distributed with this source code.
*/ function extendHandler(handler) {
return {
...{
isSelfClosing: false,
isInline: true,
allowsEmpty: false,
excludeClosing: false,
skipLastLineBreak: false,
strictMatch: false,
breakBefore: false,
breakStart: false,
breakEnd: false,
breakAfter: false
},
...handler
};
}
/*
* Copyright (c) 2022.
* Author Peter Placzek (tada5hi)
* For the full copyright and license information,
* view the LICENSE file that was distributed with this source code.
*/ function lastArrayElement(arr) {
if (arr.length) {
return arr[arr.length - 1];
}
return undefined;
}
/*
* Copyright (c) 2022-2023.
* Author Peter Placzek (tada5hi)
* For the full copyright and license information,
* view the LICENSE file that was distributed with this source code.
*/ function escapeCharacters(str, noQuotes) {
if (!str) {
return str;
}
const replacements = {
'&': '&',
'<': '<',
'>': '>',
' ': ' ',
'\r\n': '
',
'\r': '
',
'\n': '
'
};
if (noQuotes !== false) {
replacements['"'] = '"';
replacements['\''] = ''';
replacements['`'] = '`';
}
str = str.replace(/ {2}|\r\n|[&<>\r\n'"`]/g, (match)=>replacements[match] || match);
return str;
}
const VALID_SCHEME_REGEX = /^(https?|s?ftp|mailto|spotify|skype|ssh|teamspeak|tel):|(\/\/)|data:image\/(png|bmp|gif|p?jpe?g);/i;
function escapeUriScheme(url) {
let path = [];
// If there is a : before a / then it has a scheme
const hasScheme = /^[^/]*:/i;
// Has no scheme or a valid scheme
if (!url || !hasScheme.test(url) || VALID_SCHEME_REGEX.test(url)) {
return url;
}
if (typeof window !== 'undefined' && window.location) {
const { location } = window;
path = location.pathname.split('/');
path.pop();
return `${location.protocol}//${location.host}${path.join('/')}/${url}`;
}
return url;
}
/*
* Copyright (c) 2021.
* Author Peter Placzek (tada5hi)
* For the full copyright and license information,
* view the LICENSE file that was distributed with this source code.
*/ // eslint-disable-next-line @typescript-eslint/ban-types
function hasOwnProperty(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
}
/*
* Copyright (c) 2022-2022.
* Author Peter Placzek (tada5hi)
* For the full copyright and license information,
* view the LICENSE file that was distributed with this source code.
*/ function getObjectPathValue(data, key) {
const index = key.indexOf('.');
const currentKey = index === -1 ? key : key.substring(0, index);
if (index === -1) {
return data[currentKey];
}
if (!isObject(data[currentKey])) {
return undefined;
}
const nextKey = key.substring(currentKey.length + 1);
return getObjectPathValue(data[currentKey], nextKey);
}
function isObject(item) {
return !!item && typeof item === 'object' && !Array.isArray(item);
}
function formatString(str, obj) {
return str.replace(/\{([^}]+)\}/g, (match, group)=>{
let escape = true;
if (group.charAt(0) === '!') {
escape = false;
group = group.substring(1);
}
if (group === '0') {
escape = false;
}
if (typeof obj[group] === 'undefined') {
return match;
}
return escape ? escapeCharacters(obj[group], true) : obj[group];
});
}
/**
*
* @param one
* @param two
*/ function isMatch(one, two) {
if (!one) {
return true;
}
if (Array.isArray(one)) {
if (typeof two !== 'string' && typeof two !== 'number') {
return false;
}
return one.some((item)=>typeof item === 'string' && item.trim() === `${two}`.trim());
}
if (!isObject(one) || !isObject(two)) {
return false;
}
const keys = Object.keys(one);
for(let i = 0; i < keys.length; i++){
if (!hasOwnProperty(one, keys[i]) || !hasOwnProperty(two, keys[i])) {
return false;
}
if (!one[keys[i]]) {
continue;
}
if (!isMatch(one[keys[i]], two[keys[i]])) {
return false;
}
}
return true;
}
function isHTMLConditionMatch(condition, token) {
if (!condition.tag && !condition.attribute) {
return false;
}
if (condition.tag) {
if (condition.tag !== token.name.toLowerCase()) {
return false;
}
}
if (condition.attribute) {
const keys = Object.keys(condition.attribute);
for(let i = 0; i < keys.length; i++){
if (!isMatch(condition.attribute, token.attrs)) {
return false;
}
}
}
return true;
}
function isHandlerMatch(handler, token) {
if (typeof handler.conditions === 'undefined') {
return false;
}
for(let i = 0; i < handler.conditions.length; i++){
if (isHTMLConditionMatch(handler.conditions[i], token)) {
return true;
}
}
return false;
}
function findHandlerForHTMLToken(handlers, token) {
if (typeof token === 'undefined') {
return undefined;
}
const items = handlers.get();
const keys = Object.keys(items);
for(let i = 0; i < keys.length; i++){
if (isHandlerMatch(items[keys[i]], token)) {
return items[keys[i]];
}
}
return undefined;
}
/*
* Copyright (c) 2022.
* Author Peter Placzek (tada5hi)
* For the full copyright and license information,
* view the LICENSE file that was distributed with this source code.
*/ exports.TokenType = void 0;
(function(TokenType) {
TokenType["OPEN"] = "open";
TokenType["CONTENT"] = "content";
TokenType["NEWLINE"] = "newline";
TokenType["CLOSE"] = "close";
})(exports.TokenType || (exports.TokenType = {}));
/*
* Copyright (c) 2022.
* Author Peter Placzek (tada5hi)
* For the full copyright and license information,
* view the LICENSE file that was distributed with this source code.
*/ class Token {
clone() {
return new Token(this.type, this.name, this.value, {
...this.attrs
}, [], this.closing ? this.closing.clone() : undefined);
}
splitAt(splitAt) {
let offsetLength;
const clone = this.clone();
const offset = this.children.indexOf(splitAt);
if (offset > -1) {
// Work out how many items are on the right side of the split
// to pass to splice()
offsetLength = this.children.length - offset;
clone.children = this.children.splice(offset, offsetLength);
}
return clone;
}
constructor(type, name, value, attrs, children, closing){
this.type = type;
this.name = name;
this.value = value;
this.attrs = attrs || [];
this.children = children || [];
this.closing = closing || null;
}
}
function tokenizeAttrs(attrs) {
let matches = [];
/**
* ([^\s=]+) Anything that's not a space or equals
* = Equals sign =
* (?:
* (?:
* (["']) The opening quote
* (
* (?:\\\2|[^\2])*? Anything that isn't the
* unescaped opening quote
* )
* \2 The opening quote again which
* will close the string
* )
* | If not a quoted string then match
* (
* (?:.(?!\s\S+=))*.? Anything that isn't part of
* [space][non-space][=] which
* would be a new attribute
* )
* )
*/ const attrRegex = /([^\s=]+)=(?:(?:(["'])((?:\\\2|[^\2])*?)\2)|((?:.(?!\s\S+=))*.))/g;
const ret = {};
// if only one attribute then remove the = from the start and
// strip any quotes
if (attrs.charAt(0) === '=' && attrs.indexOf('=', 1) < 0) {
ret.default = stripQuotes(attrs.substring(1));
} else {
if (attrs.charAt(0) === '=') {
attrs = `default${attrs}`;
}
// No need to strip quotes here, the regex will do that.
// eslint-disable-next-line no-cond-assign
while(matches = attrRegex.exec(attrs)){
ret[matches[1].toLowerCase()] = stripQuotes(matches[3]) || matches[4];
}
}
return ret;
}
function tokenizeTag(type, input, handlerManager) {
let matches = [];
let attrs;
let name;
// Extract the name and attributes from opening tags and
// just the name from closing tags.
matches = input.match(/\[([^\]\s=]+)(?:([^\]]+))?\]/);
if (type === exports.TokenType.OPEN && matches) {
name = matches[1].toLowerCase();
if (matches[2]) {
matches[2] = matches[2].trim();
attrs = tokenizeAttrs(matches[2]);
}
}
matches = input.match(/\[\/([^[\]]+)\]/);
if (type === exports.TokenType.CLOSE && matches) {
name = matches[1].toLowerCase();
}
if (type === exports.TokenType.NEWLINE) {
name = '#newline';
}
// Treat all tokens without a name and
// all unknown BBCodes as content
if (!name || (type === exports.TokenType.OPEN || type === exports.TokenType.CLOSE) && !handlerManager.get(name)) {
type = exports.TokenType.CONTENT;
name = '#';
}
return new Token(type, name, input, attrs);
}
function tokenizeBBCode(input, handlerManager) {
// The token types in reverse order of precedence
const tokenTypes = [
{
type: exports.TokenType.CONTENT,
regex: /^([^[\r\n]+|\[)/
},
{
type: exports.TokenType.NEWLINE,
regex: /^(\r\n|\r|\n)/
},
{
type: exports.TokenType.OPEN,
regex: /^\[[^[\]]+\]/
},
{
type: exports.TokenType.CLOSE,
regex: /^\[\/[^[\]]+\]/
}
];
let matches = [];
let type;
let i;
const tokens = [];
// eslint-disable-next-line no-labels,no-restricted-syntax
inputLoop: while(input.length){
i = tokenTypes.length;
while(i--){
type = tokenTypes[i].type;
// Check if the string matches any of the tokens
matches = input.match(tokenTypes[i].regex);
if (!matches || !matches[0]) {
continue;
}
// Add the match to the tokens list
tokens.push(tokenizeTag(type, matches[0], handlerManager));
// Remove the match from the string
input = input.substring(matches[0].length);
continue inputLoop;
}
// If there is anything left in the string which doesn't match
// any of the tokens then just assume it's content and add it.
if (input.length) {
tokens.push(tokenizeTag(exports.TokenType.CONTENT, input, handlerManager));
}
input = '';
}
return tokens;
}
function parseStyles(input) {
return input.split(';').filter((style)=>style.split(':')[0] && style.split(':')[1]).map((style)=>[
style.split(':')[0].trim().replace(/-./g, (c)=>c.substring(1).toUpperCase()),
style.split(':')[1].trim()
]).reduce((styleObj, style)=>({
...styleObj,
[style[0]]: style[1]
}), {});
}
const selfClosingTags = [
'area',
'base',
'br',
'col',
'embed',
'hr',
'img',
'input',
'link',
'meta',
'param',
'source',
'track',
'wbr'
];
function parseNode(node) {
const tokens = [];
switch(node.nodeType){
case nodeHtmlParser.NodeType.ELEMENT_NODE:
{
const element = node;
if (element.rawTagName) {
const children = [];
for(let i = 0; i < element.childNodes.length; i++){
children.push(...parseNode(element.childNodes[i]));
}
let closingToken;
if (selfClosingTags.indexOf(element.tagName.toLowerCase()) === -1) {
closingToken = new Token(exports.TokenType.CLOSE, element.tagName.toLowerCase(), `${element.tagName.toLowerCase()}>`);
}
const token = new Token(exports.TokenType.OPEN, element.tagName.toLowerCase(), `<${element.tagName.toLowerCase()}>`, {
...element.attrs,
...element.attrs.style ? {
style: parseStyles(element.attrs.style)
} : {},
class: Array.from(element.classList.values())
}, children, closingToken);
tokens.push(token);
} else {
for(let i = 0; i < element.childNodes.length; i++){
tokens.push(...parseNode(element.childNodes[i]));
}
}
break;
}
case nodeHtmlParser.NodeType.TEXT_NODE:
{
const text = node;
tokens.push(new Token(exports.TokenType.CONTENT, '#', text.text));
break;
}
case nodeHtmlParser.NodeType.COMMENT_NODE:
break;
}
return tokens;
}
function tokenizeHTML(input) {
const htmlElement = nodeHtmlParser.parse(input);
return parseNode(htmlElement);
}
function removeEmptyTokens(tokens, handlerManager) {
let token;
let bbcode;
/**
* Checks if all children are whitespace or not
* @private
*/ const isTokenWhiteSpace = (children)=>{
let j = children.length;
while(j--){
const { type } = children[j];
if (type === exports.TokenType.OPEN || type === exports.TokenType.CLOSE) {
return false;
}
if (type === exports.TokenType.CONTENT && /\S|\u00A0/.test(children[j].value)) {
return false;
}
}
return true;
};
let i = tokens.length;
while(i--){
// So skip anything that isn't a tag since only tags can be
// empty, content can't
token = tokens[i];
if (!token || token.type !== exports.TokenType.OPEN) {
continue;
}
bbcode = handlerManager.get(token.name);
// Remove any empty children of this tag first so that if they
// are all removed this one doesn't think it's not empty.
removeEmptyTokens(token.children, handlerManager);
if (isTokenWhiteSpace(token.children) && bbcode && !bbcode.isSelfClosing && !bbcode.allowsEmpty) {
tokens.splice(i, 1);
}
}
}
/*
* Copyright (c) 2022.
* Author Peter Placzek (tada5hi)
* For the full copyright and license information,
* view the LICENSE file that was distributed with this source code.
*/ function hasToken(arr, name, type) {
let i = arr.length;
while(i--){
if (arr[i].type === type && arr[i].name === name) {
return true;
}
}
return false;
}
function isChildAllowed(context) {
const parentBBCode = context.parent ? context.handlers.get(context.parent.name) : {};
if (context.fixInvalidChildren && parentBBCode && parentBBCode.allowedChildren) {
return parentBBCode.allowedChildren.indexOf(context.child.name || '#') > -1;
}
return true;
}
function parseTokens(context) {
let token;
let handler;
let curTok;
let clone;
let i;
let next;
const cloned = [];
const output = [];
const openTags = [];
const currentTag = ()=>lastArrayElement(openTags);
const addTag = (token)=>{
const curr = currentTag();
if (curr) {
curr.children.push(token);
} else {
output.push(token);
}
};
const closesCurrentTag = (name)=>{
const tag = currentTag();
if (!tag) {
return false;
}
const bbcode = context.handlers.get(tag.name);
if (!bbcode) {
return false;
}
return bbcode.closedBy && bbcode.closedBy.indexOf(name) > -1;
};
// eslint-disable-next-line no-cond-assign
while(token = context.items.shift()){
// eslint-disable-next-line prefer-destructuring
next = context.items[0];
/*
* Fixes any invalid children.
*
* If it is an element which isn't allowed as a child of it's
* parent then it will be converted to content of the parent
* element. i.e.
* [code]Code [b]only[/b] allows text.[/code]
* Will become:
* Code [b]only[/b] allows text.
* Instead of:
* Code only allows text.
*/ // Ignore tags that can't be children
if (!isChildAllowed({
handlers: context.handlers,
parent: currentTag(),
child: token,
fixInvalidChildren: context.fixInvalidChildren
})) {
const curr = currentTag();
// exclude closing tags of current tag
if (token.type !== exports.TokenType.CLOSE || !curr || token.name !== curr.name) {
token.name = '#';
token.type = exports.TokenType.CONTENT;
}
}
switch(token.type){
case exports.TokenType.OPEN:
// Check it this closes a parent,
// e.g. for lists [*]one [*]two
if (closesCurrentTag(token.name)) {
openTags.pop();
}
addTag(token);
handler = context.handlers.get(token.name);
// If this tag is not self-closing and it has a closing
// tag then it is open and has children so add it to the
// list of open tags. If has the closedBy property then
// it is closed by other tags so include everything as
// it's children until one of those tags is reached.
if (handler && !handler.isSelfClosing && (handler.closedBy || hasToken(context.items, token.name, exports.TokenType.CLOSE))) {
openTags.push(token);
} else if (!handler || !handler.isSelfClosing) {
token.type = exports.TokenType.CONTENT;
}
break;
case exports.TokenType.CLOSE:
{
const curr = currentTag();
// check if this closes the current tag,
// e.g. [/list] would close an open [*]
if (curr && token.name !== curr.name && closesCurrentTag(`/${token.name}`)) {
openTags.pop();
}
// If this is closing the currently open tag just pop
// the close tag off the open tags array
if (curr && token.name === curr.name) {
curr.closing = token;
openTags.pop();
// If this is closing an open tag that is the parent of
// the current tag then clone all the tags including the
// current one until reaching the parent that is being
// closed. Close the parent and then add the clones back
// in.
} else if (hasToken(openTags, token.name, exports.TokenType.OPEN)) {
// Remove the tag from the open tags
// eslint-disable-next-line no-cond-assign
while(curTok = openTags.pop()){
// If it's the tag that is being closed then
// discard it and break the loop.
if (curTok.name === token.name) {
curTok.closing = token;
break;
}
// Otherwise clone this tag and then add any
// previously cloned tags as it's children
clone = curTok.clone();
if (cloned.length) {
const lastElement = lastArrayElement(cloned);
if (lastElement) {
clone.children.push(lastElement);
}
}
cloned.push(clone);
}
// Place block linebreak before cloned tags
if (next && next.type === exports.TokenType.NEWLINE) {
handler = context.handlers.get(token.name);
if (handler && handler.isInline === false) {
addTag(next);
context.items.shift();
}
}
// Add the last cloned child to the now current tag
// (the parent of the tag which was being closed)
const lastElement = lastArrayElement(cloned);
if (lastElement) {
addTag(lastElement);
}
// Add all the cloned tags to the open tags list
i = cloned.length;
while(i--){
openTags.push(cloned[i]);
}
cloned.length = 0;
// This tag is closing nothing so treat it as content
} else {
token.type = exports.TokenType.CONTENT;
addTag(token);
}
break;
}
case exports.TokenType.NEWLINE:
{
const curr = currentTag();
// handle things like
// [*]list\nitem\n[*]list1
// where it should come out as
// [*]list\nitem[/*]\n[*]list1[/*]
// instead of
// [*]list\nitem\n[/*][*]list1[/*]
if (curr && next && closesCurrentTag((next.type === exports.TokenType.CLOSE ? '/' : '') + next.name)) {
// skip if the next tag is the closing tag for
// the option tag, i.e. [/*]
if (!(next.type === exports.TokenType.CLOSE && next.name === curr.name)) {
handler = context.handlers.get(curr.name);
if (handler && handler.breakAfter) {
openTags.pop();
} else if (handler && handler.isInline === false && handler.breakAfter !== false) {
openTags.pop();
}
}
}
addTag(token);
break;
}
default:
addTag(token);
break;
}
}
return output;
}
/**
* Fixes any invalid nesting.
*
* If it is a block level element inside 1 or more inline elements
* than those inline elements will be split at the point where the
* block level is and the block level element placed between the split
* parts. i.e.
* [inline]A[blocklevel]B[/blocklevel]C[/inline]
* Will become:
* [inline]A[/inline][blocklevel]B[/blocklevel][inline]C[/inline]
*
*/ function fixNestingTokens(context) {
const isInline = (token)=>{
const handler = context.handlers.get(token.name);
return !handler || handler.isInline !== false;
};
context.parents = context.parents || [];
context.rootArr = context.rootArr || context.children;
let token;
let parent;
let parentIndex;
let parentParentChildren;
let right;
// This must check the length each time as it can change when
// tokens are moved to fix the nesting.
for(let i = 0; i < context.children.length; i++){
// eslint-disable-next-line no-cond-assign
if (!(token = context.children[i]) || token.type !== exports.TokenType.OPEN) {
continue;
}
if (context.insideInlineElement && !isInline(token)) {
// if this is a blocklevel element inside an inline one then
// split the parent at the block level element
parent = lastArrayElement(context.parents);
if (!parent) {
continue;
}
right = parent.splitAt(token);
parentParentChildren = context.parents.length > 1 ? context.parents[context.parents.length - 2].children : context.rootArr;
// If parent inline is allowed inside this tag, clone it and
// wrap this tags children in it.
if (isChildAllowed({
handlers: context.handlers,
parent: token,
child: parent,
fixInvalidChildren: context.fixInvalidChildren
})) {
const clone = parent.clone();
clone.children = token.children;
token.children = [
clone
];
}
parentIndex = parentParentChildren.indexOf(parent);
if (parentIndex > -1) {
// remove the block level token from the right side of
// the split inline element
right.children.splice(0, 1);
// insert the block level token and the right side after
// the left side of the inline token
parentParentChildren.splice(parentIndex + 1, 0, token, right);
// If token is a block and is followed by a newline,
// then move the newline along with it to the new parent
const next = right.children[0];
if (next && next.type === exports.TokenType.NEWLINE) {
if (!isInline(token)) {
right.children.splice(0, 1);
parentParentChildren.splice(parentIndex + 2, 0, next);
}
}
// return to parents loop as the
// children have now increased
return;
}
}
context.parents.push(token);
fixNestingTokens({
handlers: context.handlers,
children: token.children,
parents: context.parents,
insideInlineElement: context.insideInlineElement || isInline(token),
rootArr: context.rootArr,
fixInvalidChildren: context.fixInvalidChildren
});
context.parents.pop();
}
}
/* istanbul ignore next */ function normalizeTokenNewLines(context) {
const { children, parent, options, onlyRemoveBreakAfter } = context;
const childrenLength = children.length;
let token;
let left;
let right;
let parentHandler;
let handler;
let removedBreakEnd;
let removedBreakBefore;
let remove;
if (parent) {
parentHandler = context.handlers.get(parent.name);
}
let i = childrenLength;
while(i--){
// eslint-disable-next-line no-cond-assign
if (!(token = children[i])) {
continue;
}
if (token.type === exports.TokenType.NEWLINE) {
left = i > 0 ? children[i - 1] : undefined;
right = i < childrenLength - 1 ? children[i + 1] : undefined;
remove = false;
// Handle the start and end new lines
// e.g. [tag]\n and \n[/tag]
if (!onlyRemoveBreakAfter && parentHandler && parentHandler.isSelfClosing !== true) {
// First child of parent so must be opening line break
// (breakStartBlock, breakStart) e.g. [tag]\n
if (!left) {
if (parentHandler.isInline === false && options.breakStartBlock && parentHandler.breakStart !== false) {
remove = true;
}
if (parentHandler.breakStart) {
remove = true;
}
// Last child of parent so must be end line break
// (breakEndBlock, breakEnd)
// e.g. \n[/tag]
// remove last line break (breakEndBlock, breakEnd)
} else if (!removedBreakEnd && !right) {
if (parentHandler.isInline === false && options.breakEndBlock && parentHandler.breakEnd !== false) {
remove = true;
}
if (parentHandler.breakEnd) {
remove = true;
}
removedBreakEnd = remove;
}
}
if (left && left.type === exports.TokenType.OPEN) {
handler = context.handlers.get(left.name);
if (handler) {
if (!onlyRemoveBreakAfter) {
if (handler.isInline === false && options.breakAfterBlock && handler.breakAfter !== false) {
remove = true;
}
if (handler.breakAfter) {
remove = true;
}
} else if (handler.isInline === false) {
remove = true;
}
}
}
if (!onlyRemoveBreakAfter && !removedBreakBefore && right && right.type === exports.TokenType.OPEN) {
handler = context.handlers.get(right.name);
if (handler) {
if (handler.isInline === false && options.breakBeforeBlock && handler.breakBefore !== false) {
remove = true;
}
if (handler.breakBefore) {
remove = true;
}
removedBreakBefore = remove;
if (remove) {
children.splice(i, 1);
continue;
}
}
}
if (remove) {
children.splice(i, 1);
}
// reset double removedBreakBefore removal protection.
// This is needed for cases like \n\n[\tag] where
// only 1 \n should be removed but without this they both
// would be.
removedBreakBefore = false;
} else if (token.type === exports.TokenType.OPEN) {
normalizeTokenNewLines({
handlers: context.handlers,
children: token.children,
parent: token,
options,
onlyRemoveBreakAfter
});
}
}
}
function convertBBCodeToHTML(context) {
let bbcode;
let content;
let html;
let ret = '';
const isInline = (handler)=>typeof handler === 'undefined' || (typeof handler.isHtmlInline !== 'undefined' ? handler.isHtmlInline : handler.isInline) !== false;
while(context.tokens.length > 0){
const token = context.tokens.shift();
if (!token) {
continue;
}
html = '';
switch(token.type){
case exports.TokenType.OPEN:
{
const lastChild = token.children[token.children.length - 1] || {};
bbcode = context.handlers.get(token.name);
content = convertBBCodeToHTML({
tokens: [
...token.children
],
options: {
...context.options,
isRoot: false
},
handlers: context.handlers
});
if (bbcode && bbcode.html) {
const lastChildHandler = context.handlers.get(lastChild.name);
// Only add a line break to the end if this is
// blocklevel and the last child wasn't block-level
if (lastChildHandler && !isInline(bbcode) && isInline(lastChildHandler) && !bbcode.skipLastLineBreak) {
// Add placeholder br to end of block level
// elements
content += '
';
}
if (typeof bbcode.html !== 'function') {
token.attrs['0'] = content;
html = formatString(bbcode.html, token.attrs);
} else {
html = bbcode.html({
handlers: context.handlers,
token,
attributes: token.attrs,
content,
options: context.options
});
}
} else if (!context.options.lazy) {
html = token.value + content + (token.closing ? token.closing.value : '');
}
break;
}
/* istanbul ignore next */ case exports.TokenType.NEWLINE:
{
if (!context.options.isRoot) {
ret += '
';
continue;
}
ret += '
';
// Normally the div acts as a line-break with by moving
// whatever comes after onto a new line.
// If this is the last token, add an extra line-break so it
// shows as there will be nothing after it.
if (!context.tokens.length) {
ret += '
';
}
ret += '\n';
continue;
}
default:
{
html = escapeCharacters(token.value, true);
break;
}
}
ret += html;
}
return ret;
}
function convertHTMLToBBCode(context) {
context.options = context.options || {};
let output = '';
const isInline = (handler)=>typeof handler === 'undefined' || (typeof handler.isHtmlInline !== 'undefined' ? handler.isHtmlInline : handler.isInline) !== false;
while(context.tokens.length > 0){
const token = context.tokens.shift();
if (!token) {
continue;
}
switch(token.type){
case exports.TokenType.OPEN:
{
const lastChild = token.children[token.children.length - 1];
const handler = findHandlerForHTMLToken(context.handlers, token);
let content = convertHTMLToBBCode({
tokens: [
...token.children
],
options: {
...context.options,
isRoot: false
},
handlers: context.handlers
});
if (handler && handler.bbcode) {
const lastChildHandler = findHandlerForHTMLToken(context.handlers, lastChild);
if (lastChildHandler && !isInline(handler) && isInline(lastChildHandler) && !handler.skipLastLineBreak) {
// Add placeholder br to end of block level
// elements
content += '\n';
}
if (typeof handler.bbcode !== 'function') {
token.attrs['0'] = content;
output += formatString(handler.bbcode, token.attrs);
} else {
output += handler.bbcode({
handlers: context.handlers,
token,
attributes: token.attrs,
content,
options: context.options
});
}
} else if (!context.options.lazy) {
output += token.value + content + (token.closing ? token.closing.value : '');
}
break;
}
/* istanbul ignore next */ case exports.TokenType.NEWLINE:
{
if (!context.options.isRoot) {
output += '\n';
continue;
}
output += '\n';
if (!context.tokens.length) {
output += '\n';
}
break;
}
default:
{
output += escapeCharacters(token.value, true);
break;
}
}
}
return output;
}
/* istanbul ignore next */ const HandlerPreset = {
h1: {
conditions: [
{
tag: 'h1'
}
],
bbcode: '[h1]{0}[/h1]',
html: '
${context.content}`; } }, // END_COMMAND // START_COMMAND: Code code: { conditions: [ { tag: 'code' } ], isInline: false, allowedChildren: [ '#', '#newline' ], bbcode: '[code]{0}[/code]', html: '
{0}
'
},
// END_COMMAND
// START_COMMAND: Left
left: {
conditions: [
{
attribute: {
style: {
textAlign: [
'left',
'-webkit-left',
'-moz-left',
'-khtml-left'
]
}
}
}
],
isInline: false,
allowsEmpty: true,
bbcode: '[left]{0}[/left]',
html: '