fortissimo-html
Version:
Fortissimo HTML - Flexible, Forgiving, Formatting HTML Parser
248 lines (245 loc) • 9.84 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.stylizeHtml = void 0;
const copy_script_1 = require("./copy-script");
const dom_1 = require("./dom");
const characters_1 = require("./characters");
const elements_1 = require("./elements");
const DEFAULT_OPTIONS = {
dark: true,
font: '12px Menlo, "Courier New", monospace',
includeCopyScript: true,
outerTag: 'html',
showWhitespace: false,
stylePrefix: 'fh-',
tabSize: 8,
title: 'Stylized HTML'
};
const DEFAULT_DARK_THEME = {
attrib: '#9CDCFE',
background: '#1E1E1E',
bg_whitespace: '#555555',
comment: '#699856',
entity: '#66BBBB',
error: '#CC4444',
foreground: '#D4D4D4',
invalid: '#FF00FF',
markup: '#808080',
tag: '#569CD6',
value: '#CE9178',
warning: '#F49810',
whitespace: '#605070'
};
const DEFAULT_LIGHT_THEME = {
attrib: '#5544FF',
background: '#FFFFFF',
bg_whitespace: '#CCCCCC',
comment: '#80B0B0',
entity: '#0088DD',
error: '#D40000',
foreground: '#222222',
invalid: '#FF00FF',
markup: '#808080',
tag: '#000080',
value: '#008088',
warning: '#F49810',
whitespace: '#C0D0F0'
};
const COLORS = Object.keys(DEFAULT_LIGHT_THEME);
function stylizeHtml(elem, options) {
options = processOptions(options);
const fullDocument = (options.outerTag === 'html');
const tag = fullDocument ? 'body' : options.outerTag;
return (fullDocument ? `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>${options.title}</title>
<style>
${generateCss(options)} </style>
</head>
` : '') +
`<${tag} class="${options.stylePrefix}html">${stylize(elem, options)}${options.includeCopyScript ?
'<script>' + (0, copy_script_1.getCopyScript)(options.stylePrefix) + '</script>'
: ''}</${tag}>` + (fullDocument ? '</html>' : '');
}
exports.stylizeHtml = stylizeHtml;
function stylize(elem, options) {
const pf = options.stylePrefix;
const ws = options.showWhitespace;
if (elem instanceof dom_1.CommentElement)
return markup(elem.toString(), pf, 'comment', ws, false);
else if (elem instanceof dom_1.CData) {
return markup('<![CDATA[', pf, 'markup', false, false) +
markup(elem.content, pf, null, ws, false) +
markup(']]>', pf, 'markup', false, false);
}
else if (elem instanceof dom_1.DocType) {
return elem.toString().replace(/("[^"]*?"[ \n\r\t\f]*|[^ ">]+[ \n\r\t\f]*|.+)/g, match => {
if (match.startsWith('"'))
return markup(match, pf, 'value', ws, false);
else if (/^\w/.test(match))
return markup(match, pf, 'attrib', ws, false);
else
return markup(match, pf, 'markup', ws, false);
});
}
else if (elem instanceof dom_1.DeclarationElement || elem instanceof dom_1.ProcessingElement)
return markup(elem.toString(), pf, 'markup', ws, false);
else if (elem instanceof dom_1.TextElement)
return markup(elem.toString(), pf, null, ws, !elem.parent || !elements_1.NO_ENTITIES_ELEMENTS.has(elem.parent.tagLc));
else if (elem instanceof dom_1.UnmatchedClosingTag)
return markup(elem.toString(), pf, 'error', ws, false);
else if (elem instanceof dom_1.DomNode) {
const result = [];
const badTerminator = elem.badTerminator;
let tagClass = 'tag';
if (!elem.synthetic) {
if (!(0, characters_1.isAllPCENChar)(elem.tag))
tagClass = 'warning';
result.push(markup('<', pf, badTerminator !== null ? 'error' : 'markup', false, false));
result.push(markup(elem.tag, pf, badTerminator ? 'error' : tagClass, false, false));
elem.attributes.forEach((attrib, index) => {
result.push(markup(elem.spacing[index], pf, null, ws, false));
result.push(markup(attrib, pf, attrib === '/' ? 'error' : 'attrib', false, false));
result.push(markup(elem.equals[index] || '', pf, null, ws, false));
const quote = elem.quotes[index];
const value = (0, dom_1.OQ)(quote) + elem.values[index] + (0, dom_1.CQ)(quote);
if (!quote && /["'=<>`]/.test(value))
result.push(markup(value, pf, 'warning', false, false));
else
result.push(markup(value, pf, 'value', ws, true));
});
result.push(markup(elem.innerWhitespace, pf, null, ws, false));
if (badTerminator !== null)
result.push(markup(badTerminator, pf, 'error', false, false));
else if (elem.closureState === dom_1.ClosureState.SELF_CLOSED)
result.push(markup('/>', pf, 'markup', false, false));
else
result.push(markup('>', pf, 'markup', false, false));
}
if (elem.children)
elem.children.forEach(child => result.push(stylize(child, options)));
if (!elem.synthetic && elem.closureState === dom_1.ClosureState.EXPLICITLY_CLOSED) {
const terminated = elem.endTagText.endsWith('>');
result.push(markup('</', pf, terminated ? 'markup' : 'error', false, false));
if (terminated) {
result.push(markup(elem.endTagText.substring(2, elem.endTagText.length - 1), pf, tagClass, ws, false));
result.push(markup('>', pf, 'markup', false, false));
}
else
result.push(markup(elem.endTagText.substr(2), pf, 'error', false, false));
}
return result.join('');
}
return null;
}
function processOptions(options) {
options = Object.assign(Object.assign({}, DEFAULT_OPTIONS), options || {});
options.colors = Object.assign(Object.assign({}, options.dark ? DEFAULT_DARK_THEME : DEFAULT_LIGHT_THEME), options.colors);
return options;
}
function generateCss(options) {
const prefix = options.stylePrefix;
let css = ` .${prefix}html {
background-color: ${options.colors.background};
color: ${options.colors.foreground};
font: ${options.font};
-moz-tab-size: ${options.tabSize};
tab-size: ${options.tabSize};
white-space: pre;
}
.${prefix}tab {
color: ${options.colors.whitespace};
}
.${prefix}tab::before {
content: "→";
display: inline-block;
overflow-x: visible;
width: 0;
}
`;
COLORS.forEach(color => {
const property = color.startsWith('bg_') ? 'background-color' : 'color';
const value = options.colors[color];
css += ` .${prefix}${color} { ${property}: ${value}; }\n`;
});
return css;
}
const whitespaces = {
' ': '·',
'\t': '\t',
'\n': '↵\n',
'\f': '↧\f',
'\r': '␍\r',
'\r\n': '␍↵\r\n',
'\xA0': '•'
};
function markup(s, prefix, qlass, markWhitespace, markEntities, checkInvalid = true) {
if (!s)
return '';
else if (!qlass && !markWhitespace && !markEntities && !checkInvalid)
return (0, characters_1.minimalEscape)(s);
else if (markWhitespace) {
return s.split(/([ \n\r\f\xA0]+|\t)/).map((match, index) => {
if (index % 2 === 1) {
match = match.replace(/\r\n|\n|\r|./g, ch => whitespaces[ch]);
return markup(match, prefix, match === '\t' ? 'tab' : 'whitespace', false, false, false);
}
else if (match) {
return match.split(/([\u2000-\u200A]|\u202F|\u205F|\u3000)/).map((match2, index2) => {
if (index2 % 2 === 1)
return markup(match2, prefix, 'bg_whitespace', false, false, false);
else
return markup(match2, prefix, qlass, false, markEntities, checkInvalid);
}).join('');
}
else
return '';
}).join('');
}
else if (checkInvalid) {
s = (0, characters_1.replaceIsolatedSurrogates)(s);
return s.split(/([\x00-\x08\x0B\x0E-\x1F\x7F-\x9F\uFFFD]+)/).map((match, index) => {
if (index % 2 === 1)
return markup('�'.repeat(match.length), prefix, 'invalid', false, false, false);
else {
return markup(match, prefix, qlass, false, markEntities, false);
}
}).join('');
}
else if (markEntities) {
const sb = [];
(0, characters_1.separateEntities)(s).forEach((part, index) => {
if (index % 2 === 0)
sb.push(markup(part, prefix, qlass, false, false, false));
else {
const eClass = getEntityClass(part, qlass && qlass.endsWith('value'));
sb.push(markup(part, prefix, eClass, false, false, false));
}
});
return sb.join('');
}
return `<span class="${prefix}${qlass}">${(0, characters_1.minimalEscape)(s)}</span>`;
}
function getEntityClass(entity, forAttribValue) {
let bestCase = 'entity';
entity = entity.substr(1);
if (!entity.endsWith(';')) {
if (forAttribValue)
return 'value';
else
bestCase = 'warning';
}
else
entity = entity.substr(0, entity.length - 1);
let cp;
if (entity.toLowerCase().startsWith('#x'))
return isNaN(cp = parseInt(entity.substr(2), 16)) ||
!(0, characters_1.isValidEntityCodepoint)(cp) ? 'error' : (cp === 0xFFFD ? 'invalid' : bestCase);
if (entity.toLowerCase().startsWith('#'))
return isNaN(cp = parseInt(entity.substr(1), 10)) ||
!(0, characters_1.isValidEntityCodepoint)(cp) ? 'error' : (cp === 0xFFFD ? 'invalid' : bestCase);
return (0, characters_1.isKnownNamedEntity)(entity) ? 'entity' : 'warning';
}
//# sourceMappingURL=stylizer.js.map