m (Lift whitespace text node filtering up to fix using children array when only one child is emitted) |
m (Fix bad joke) |
||
Line 3: | Line 3: | ||
* Transforms filthy HTML into beautiful Dorui |
* Transforms filthy HTML into beautiful Dorui |
||
* |
* |
||
− | * UI-js? I |
+ | * UI-js? I hardly know her! |
* |
* |
||
* @author Dorumin |
* @author Dorumin |
Revision as of 03:13, 5 March 2021
/* HTML to Dorui
*
* Transforms filthy HTML into beautiful Dorui
*
* UI-js? I hardly know her!
*
* @author Dorumin
*/
(function() {
var ui;
var refs = {};
var SPECIALS = [
'html',
'text',
'child',
'children',
'attrs',
'props',
'events',
'style',
'classes'
];
// Tries to convert a string to a tree of nodes
// Done by creating a DOMParser and parsing it as html
function stringToNodes(string) {
var parser = new DOMParser();
try {
var doc = parser.parseFromString(string, 'text/html');
return Array.from(doc.body.childNodes);
} catch(e) {
// We don't have valid HTML, chief
// Which is _extremely_ odd because having HTML conversion fail is really hard
console.error('Error while parsing HTML', e);
return [];
}
}
// Converts an array of nodes into Dorui calls
function nodesToDorui(nodes) {
if (nodes.length === 1) {
return doruiCall(nodes[0], 0) + ';';
} else {
return doruiFragCall(nodes, 0) + ';';
}
}
function indent(amount) {
return new Array(amount * 4 + 1).join(' ');
}
function isEmptyObject(object) {
return Object.keys(object).length === 0;
}
function quoted(string) {
return "'" + string.replace(/'/g, "\\'").replace(/\n/g, '\\n') + "'";
}
function camelCased(string) {
return string.replace(/-(\w)/g, function(_, letter) {
return letter.toUpperCase();
});
}
// Takes a string and returns it quoted if it's needed as an object key
function objectKey(key) {
if (key === '') {
// Empty strings have to be quoted
return "''";
}
if (!Number.isNaN(Number(key))) {
// If the key is completely a number, it's valid without quotes
return key;
}
if (!Number.isNaN(Number(key.charAt(0)))) {
// First character is a number, quote it
return quoted(key);
}
if (/[^a-zA-Z0-9_$]/.test(key)) {
// Conservatively test against any non-standard characters in the identifier
// Unicode stuffs are valid in identifiers, but I don't want to risk it
// Because characters like !, ., ", and ; exist
return quoted(key);
}
// Should be safe, consists entirely of a-z A-Z 0-9, and doesn't start with a number
return key;
}
function objectValue(key, value, tabs) {
switch (key) {
case 'attrs':
case 'style':
var json = '{';
for (var key in value) {
var val = value[key];
if (typeof val === 'boolean') {
val = String(val);
} else {
val = quoted(String(val));
}
json += '\n' + indent(tabs) + objectKey(key) + ': ' + val + ',';
}
json = json.slice(0, -1) + '\n' + indent(tabs - 1) + '}';
return json;
case 'child':
return doruiCall(value, tabs).trimStart();
case 'children':
var json = '[';
for (var key in value) {
var node = value[key];
json += '\n' + doruiCall(node, tabs) + ',';
}
json = json.slice(0, -1) + '\n' + indent(tabs - 1) + ']';
return json;
case 'text':
return quoted(value);
case 'events':
// TODO, unreachable rn as nodesToDorui doesn't transform on- attributes
return '/* NOT YET IMPLEMENTED */';
case 'classes':
case 'props':
case 'html':
// Cannot be generated from nodesToDorui
return '/* UNREACHABLE */';
default:
return quoted(String(value));
}
}
var OPTIONS_SORT = {
id: -5,
class: -4,
classes: -3,
attrs: -2,
style: -1,
// Everything else goes here
text: 1,
child: 2,
children: 3,
props: 4
};
function sortedOptionsEntries(options) {
return Object.keys(options)
.sort(function(a, b) {
var aScore = OPTIONS_SORT[a] || 0;
var bScore = OPTIONS_SORT[b] || 0;
return aScore - bScore;
})
.map(function(key) {
return [key, options[key]];
});
}
function getElementOptions(node, tabs) {
var options = {};
var childNodes = Array.from(node.childNodes)
.filter(function(node) {
return node.nodeType !== Node.TEXT_NODE || node.textContent.trim() !== '';
});
if (childNodes.length !== 0) {
if (childNodes.length === 1) {
var childNode = childNodes[0];
if (childNode.nodeType === Node.TEXT_NODE) {
if (childNode.textContent.trim() !== '') {
options.text = childNode.textContent;
}
} else {
options.child = childNode;
}
} else {
options.children = Array.from(childNodes);
}
}
if (node.attributes.length !== 0) {
for (var i = 0; i < node.attributes.length; i++) {
var attr = node.attributes[i];
if (attr.name === 'style') {
options.style = {};
for (var j = 0; j < node.style.length; j++) {
var styleName = node.style.item(j);
var styleValue = node.style[styleName];
options.style[camelCased(styleName)] = styleValue;
}
} else if (SPECIALS.includes(attr.name)) {
if (!options.hasOwnProperty('attrs')) {
options.attrs = {};
}
options.attrs[attr.name] = attr.value;
} else {
options[attr.name] = attr.value;
}
}
}
if (isEmptyObject(options)) {
return '';
}
var object = '{';
sortedOptionsEntries(options).forEach(function(pair) {
var key = pair[0];
var value = pair[1];
object += '\n' + indent(tabs) + objectKey(key) + ': '+ objectValue(key, value, tabs + 1) + ',';
});
// Remove leading comma and add last brace
object = object.slice(0, -1) + '\n' + indent(tabs - 1) + '}';
return object;
}
function doruiCall(node, tabs) {
switch (node.nodeType) {
case Node.TEXT_NODE:
return indent(tabs) + quoted(node.textContent);
case Node.ELEMENT_NODE:
var alias = node.nodeName.toLowerCase();
return indent(tabs) + 'ui.' + alias + '(' + getElementOptions(node, tabs + 1) + ')';
default:
return indent(tabs) + '/* INVALID NODE TYPE FOUND */';
}
}
function doruiFragCall(nodes, tabs) {
var code = 'ui.frag([';
for (var i in nodes) {
var node = nodes[i];
if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() === '') continue;
code += '\n' + doruiCall(node, tabs + 1) + ',';
}
// Remove leading comma and add closing braces
code = code.slice(0, -1) + '\n' + indent(tabs) + '])';
return code;
}
function preload() {
dev.highlight.useTheme('vs2015');
dev.highlight.loadLanguage('js').then(start);
}
function start() {
ui = dev.dorui;
var $modal = dev.showCustomModal('HTML to Dorui', {
id: 'HTMLToDoruiModal',
width: 500,
content: ui.frag([
ui.p({
text: 'Paste HTML into the textarea below, and click on "Convert" to turn it into valid Dorui!'
}),
ui.p({
text: 'Remember that this is all turned into a single Dorui tree, you may want to factor it into smaller functions',
}),
refs.textarea = ui.textarea({
style: {
display: 'block',
height: '200px',
width: '100%',
maxWidth: '100%'
}
})
]),
buttons: [
{
message: 'Close',
handler: function() {
dev.showCustomModal.closeModal($modal);
}
},
{
message: 'Convert',
defaultButton: true,
handler: function() {
var nodes = stringToNodes(refs.textarea.value);
if (nodes.length === 0) {
alert('No nodes found. Are you sure you\'re passing valid HTML?');
return;
}
if (nodes.length === 1 && nodes[0].nodeType === Node.TEXT_NODE) {
alert('A single node found, and it was a text node. Are you sure you\'re passing valid HTML?');
return;
}
var code = nodesToDorui(nodes);
var highlightedHTML = dev.highlight.highlight('javascript', code).value;
highlightedHTML = highlightedHTML.replace(/^(\s*)ui.(\w+)\(/gm, function(_, indent, method) {
return indent + 'ui.' + '<span class="hljs-name">' + method + '</span>(';
});
var $outputModal = dev.showCustomModal('Output', {
content: ui.frag([
'Here is your beautiful Dorui:',
ui.pre({
class: 'hljs',
style: {
lineHeight: '14px'
},
// SAFETY: highlightedHTML is spat out from Highlight-js
// and there's minor changes to highlight Dorui methods
html: highlightedHTML
})
]),
buttons: [
{
message: 'Close',
handler: function() {
dev.showCustomModal.closeModal($outputModal);
}
},
{
message: 'Copy',
defaultButton: true,
handler: function() {
navigator.clipboard.writeText(code);
}
}
]
});
}
}
]
});
}
importArticles({
type: 'script',
articles: [
'u:dev:MediaWiki:Dorui.js',
'u:dev:MediaWiki:ShowCustomModal.js',
'u:dev:MediaWiki:Highlight-js.js'
]
}).then(preload);
})();