m (Fix bad joke) |
m (+config, +events, +classes, +frag warning, +leading commas, +trimming) |
||
Line 112: | Line 112: | ||
} |
} |
||
− | json += '\n' + indent(tabs) + objectKey(key) + ': ' + val + ','; |
+ | json += '\n' + indent(tabs + 1) + objectKey(key) + ': ' + val + ','; |
} |
} |
||
− | + | if (refs.commasBox.checked) { |
|
+ | json = json.slice(0, -1); |
||
⚫ | |||
+ | |||
⚫ | |||
return json; |
return json; |
||
Line 126: | Line 130: | ||
var node = value[key]; |
var node = value[key]; |
||
− | json += '\n' + doruiCall(node, tabs) + ','; |
+ | json += '\n' + doruiCall(node, tabs + 1) + ','; |
} |
} |
||
− | + | if (refs.commasBox.checked) { |
|
+ | json = json.slice(0, -1); |
||
+ | } |
||
+ | |||
⚫ | |||
return json; |
return json; |
||
case 'text': |
case 'text': |
||
− | return quoted(value); |
+ | return quoted(refs.trimBox.checked ? value.trim() : value); |
⚫ | |||
− | // TODO, unreachable rn as nodesToDorui doesn't transform on- attributes |
||
⚫ | |||
case 'classes': |
case 'classes': |
||
+ | var json = '['; |
||
+ | |||
+ | for (var key in value) { |
||
+ | var cls = value[key]; |
||
+ | |||
+ | json += '\n' + indent(tabs + 1) + quoted(cls) + ','; |
||
+ | } |
||
+ | |||
+ | if (refs.commasBox.checked) { |
||
+ | json = json.slice(0, -1); |
||
+ | } |
||
+ | |||
+ | json += '\n' + indent(tabs) + ']'; |
||
+ | |||
⚫ | |||
+ | case 'events': |
||
+ | var json = '{'; |
||
+ | |||
+ | for (var key in value) { |
||
+ | var val = value[key]; |
||
+ | var body = val.trim().replace(/^/gm, indent(tabs + 2)); |
||
+ | |||
+ | val = 'function() {\n' + body + '\n' + indent(tabs + 1) + '}'; |
||
+ | |||
+ | json += '\n' + indent(tabs + 1) + objectKey(key) + ': ' + val + ','; |
||
+ | } |
||
+ | |||
+ | if (refs.commasBox.checked) { |
||
+ | json = json.slice(0, -1); |
||
+ | } |
||
+ | |||
+ | json += '\n' + indent(tabs) + '}'; |
||
+ | |||
+ | return json; |
||
case 'props': |
case 'props': |
||
case 'html': |
case 'html': |
||
Line 212: | Line 251: | ||
options.style[camelCased(styleName)] = styleValue; |
options.style[camelCased(styleName)] = styleValue; |
||
} |
} |
||
− | } else if ( |
+ | } else if (refs.classesBox.checked && attr.name === 'class') { |
+ | options.classes = attr.value.split(/\s+/g).filter(Boolean); |
||
+ | } else if (refs.eventsBox.checked && attr.name.slice(0, 2) === 'on') { |
||
+ | if (!options.hasOwnProperty('events')) { |
||
+ | options.events = {}; |
||
+ | } |
||
+ | |||
+ | options.events[attr.name.slice(2)] = attr.value; |
||
+ | } else if (refs.attrsBox.checked || SPECIALS.includes(attr.name)) { |
||
if (!options.hasOwnProperty('attrs')) { |
if (!options.hasOwnProperty('attrs')) { |
||
options.attrs = {}; |
options.attrs = {}; |
||
Line 235: | Line 282: | ||
var value = pair[1]; |
var value = pair[1]; |
||
− | object += '\n' + indent(tabs) + objectKey(key) + ': '+ objectValue(key, value, tabs + 1) + ','; |
+ | object += '\n' + indent(tabs + 1) + objectKey(key) + ': '+ objectValue(key, value, tabs + 1) + ','; |
}); |
}); |
||
// Remove leading comma and add last brace |
// Remove leading comma and add last brace |
||
+ | if (refs.commasBox.checked) { |
||
⚫ | |||
+ | object = object.slice(0, -1); |
||
+ | } |
||
+ | |||
+ | object += '\n' + indent(tabs) + '}'; |
||
return object; |
return object; |
||
Line 247: | Line 298: | ||
switch (node.nodeType) { |
switch (node.nodeType) { |
||
case Node.TEXT_NODE: |
case Node.TEXT_NODE: |
||
− | return indent(tabs) + quoted(node.textContent); |
+ | return indent(tabs) + quoted(refs.trimBox.checked ? node.textContent.trim() : node.textContent); |
case Node.ELEMENT_NODE: |
case Node.ELEMENT_NODE: |
||
var alias = node.nodeName.toLowerCase(); |
var alias = node.nodeName.toLowerCase(); |
||
− | return indent(tabs) + 'ui.' + alias + '(' + getElementOptions(node, tabs |
+ | return indent(tabs) + 'ui.' + alias + '(' + getElementOptions(node, tabs) + ')'; |
default: |
default: |
||
return indent(tabs) + '/* INVALID NODE TYPE FOUND */'; |
return indent(tabs) + '/* INVALID NODE TYPE FOUND */'; |
||
Line 268: | Line 319: | ||
// Remove leading comma and add closing braces |
// Remove leading comma and add closing braces |
||
+ | if (refs.commasBox.checked) { |
||
⚫ | |||
+ | code = code.slice(0, -1); |
||
+ | } |
||
+ | |||
+ | code += '\n' + indent(tabs) + '])'; |
||
return code; |
return code; |
||
Line 290: | Line 345: | ||
ui.p({ |
ui.p({ |
||
text: 'Remember that this is all turned into a single Dorui tree, you may want to factor it into smaller functions', |
text: 'Remember that this is all turned into a single Dorui tree, you may want to factor it into smaller functions', |
||
+ | }), |
||
+ | ui.details({ |
||
+ | children: [ |
||
+ | ui.summary({ |
||
+ | text: 'Show configuration' |
||
+ | }), |
||
+ | ui.p({ |
||
+ | children: [ |
||
+ | refs.trimBox = ui.input({ |
||
+ | type: 'checkbox', |
||
+ | id: 'text-trim-checkbox', |
||
+ | props: { |
||
+ | checked: true |
||
+ | } |
||
+ | }), |
||
+ | ui.label({ |
||
+ | for: 'text-trim-checkbox', |
||
+ | text: 'Trim text content and text nodes' |
||
+ | }) |
||
+ | ] |
||
+ | }), |
||
+ | ui.p({ |
||
+ | children: [ |
||
+ | refs.commasBox = ui.input({ |
||
+ | type: 'checkbox', |
||
+ | id: 'leading-commas-checkbox', |
||
+ | props: { |
||
+ | checked: true |
||
+ | } |
||
+ | }), |
||
+ | ui.label({ |
||
+ | for: 'leading-commas-checkbox', |
||
+ | text: 'Remove leading commas' |
||
+ | }) |
||
+ | ] |
||
+ | }), |
||
+ | ui.p({ |
||
+ | children: [ |
||
+ | refs.eventsBox = ui.input({ |
||
+ | type: 'checkbox', |
||
+ | id: 'convert-events-checkbox', |
||
+ | props: { |
||
+ | checked: true |
||
+ | } |
||
+ | }), |
||
+ | ui.label({ |
||
+ | for: 'convert-events-checkbox', |
||
+ | text: 'Convert on- attributes to `events`' |
||
+ | }) |
||
+ | ] |
||
+ | }), |
||
+ | ui.p({ |
||
+ | children: [ |
||
+ | refs.classesBox = ui.input({ |
||
+ | type: 'checkbox', |
||
+ | id: 'classes-force-checkbox', |
||
+ | props: { |
||
+ | checked: false |
||
+ | } |
||
+ | }), |
||
+ | ui.label({ |
||
+ | for: 'classes-force-checkbox', |
||
+ | text: 'Use `classes` for class attributes' |
||
+ | }) |
||
+ | ] |
||
+ | }), |
||
+ | ui.p({ |
||
+ | children: [ |
||
+ | refs.attrsBox = ui.input({ |
||
+ | type: 'checkbox', |
||
+ | id: 'attrs-force-checkbox', |
||
+ | props: { |
||
+ | checked: false |
||
+ | } |
||
+ | }), |
||
+ | ui.label({ |
||
+ | for: 'attrs-force-checkbox', |
||
+ | text: 'Force the `attrs` object for attributes. Not recommended' |
||
+ | }) |
||
+ | ] |
||
+ | }) |
||
+ | ] |
||
}), |
}), |
||
refs.textarea = ui.textarea({ |
refs.textarea = ui.textarea({ |
||
Line 333: | Line 470: | ||
var $outputModal = dev.showCustomModal('Output', { |
var $outputModal = dev.showCustomModal('Output', { |
||
content: ui.frag([ |
content: ui.frag([ |
||
− | + | nodes.length > 1 && ui.p({ |
|
+ | text: 'Note: There was more than one top-level node, so your tree was converted to a document fragment' |
||
+ | }), |
||
+ | ui.p({ |
||
+ | text: 'Here is your beautiful Dorui:', |
||
+ | }), |
||
ui.pre({ |
ui.pre({ |
||
class: 'hljs', |
class: 'hljs', |
Revision as of 14:58, 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 + 1) + objectKey(key) + ': ' + val + ',';
}
if (refs.commasBox.checked) {
json = json.slice(0, -1);
}
json += '\n' + indent(tabs) + '}';
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 + 1) + ',';
}
if (refs.commasBox.checked) {
json = json.slice(0, -1);
}
json += '\n' + indent(tabs) + ']';
return json;
case 'text':
return quoted(refs.trimBox.checked ? value.trim() : value);
case 'classes':
var json = '[';
for (var key in value) {
var cls = value[key];
json += '\n' + indent(tabs + 1) + quoted(cls) + ',';
}
if (refs.commasBox.checked) {
json = json.slice(0, -1);
}
json += '\n' + indent(tabs) + ']';
return json;
case 'events':
var json = '{';
for (var key in value) {
var val = value[key];
var body = val.trim().replace(/^/gm, indent(tabs + 2));
val = 'function() {\n' + body + '\n' + indent(tabs + 1) + '}';
json += '\n' + indent(tabs + 1) + objectKey(key) + ': ' + val + ',';
}
if (refs.commasBox.checked) {
json = json.slice(0, -1);
}
json += '\n' + indent(tabs) + '}';
return json;
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 (refs.classesBox.checked && attr.name === 'class') {
options.classes = attr.value.split(/\s+/g).filter(Boolean);
} else if (refs.eventsBox.checked && attr.name.slice(0, 2) === 'on') {
if (!options.hasOwnProperty('events')) {
options.events = {};
}
options.events[attr.name.slice(2)] = attr.value;
} else if (refs.attrsBox.checked || 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 + 1) + objectKey(key) + ': '+ objectValue(key, value, tabs + 1) + ',';
});
// Remove leading comma and add last brace
if (refs.commasBox.checked) {
object = object.slice(0, -1);
}
object += '\n' + indent(tabs) + '}';
return object;
}
function doruiCall(node, tabs) {
switch (node.nodeType) {
case Node.TEXT_NODE:
return indent(tabs) + quoted(refs.trimBox.checked ? node.textContent.trim() : node.textContent);
case Node.ELEMENT_NODE:
var alias = node.nodeName.toLowerCase();
return indent(tabs) + 'ui.' + alias + '(' + getElementOptions(node, tabs) + ')';
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
if (refs.commasBox.checked) {
code = code.slice(0, -1);
}
code += '\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',
}),
ui.details({
children: [
ui.summary({
text: 'Show configuration'
}),
ui.p({
children: [
refs.trimBox = ui.input({
type: 'checkbox',
id: 'text-trim-checkbox',
props: {
checked: true
}
}),
ui.label({
for: 'text-trim-checkbox',
text: 'Trim text content and text nodes'
})
]
}),
ui.p({
children: [
refs.commasBox = ui.input({
type: 'checkbox',
id: 'leading-commas-checkbox',
props: {
checked: true
}
}),
ui.label({
for: 'leading-commas-checkbox',
text: 'Remove leading commas'
})
]
}),
ui.p({
children: [
refs.eventsBox = ui.input({
type: 'checkbox',
id: 'convert-events-checkbox',
props: {
checked: true
}
}),
ui.label({
for: 'convert-events-checkbox',
text: 'Convert on- attributes to `events`'
})
]
}),
ui.p({
children: [
refs.classesBox = ui.input({
type: 'checkbox',
id: 'classes-force-checkbox',
props: {
checked: false
}
}),
ui.label({
for: 'classes-force-checkbox',
text: 'Use `classes` for class attributes'
})
]
}),
ui.p({
children: [
refs.attrsBox = ui.input({
type: 'checkbox',
id: 'attrs-force-checkbox',
props: {
checked: false
}
}),
ui.label({
for: 'attrs-force-checkbox',
text: 'Force the `attrs` object for attributes. Not recommended'
})
]
})
]
}),
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([
nodes.length > 1 && ui.p({
text: 'Note: There was more than one top-level node, so your tree was converted to a document fragment'
}),
ui.p({
text: '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);
})();