Created
September 29, 2019 17:51
-
-
Save Quacky2200/19aac2fad6d56f148db17c9194a85f66 to your computer and use it in GitHub Desktop.
This is a tag.js alternative written in TypeScript; most specifically for react development. You'd likely use JSX, but this is here as I was used to using pug-like strings before.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'core-js'; | |
import React from 'react'; | |
const matchAll = require('string.prototype.matchall'); | |
interface iTagObject { | |
name: string, | |
attributes: any, | |
content: any | |
} | |
class TagObject implements iTagObject { | |
public name:string; | |
public attributes:any; | |
public content:any; | |
constructor(name:string, attributes:any, content:any) { | |
this.name = name; | |
this.attributes = attributes; | |
this.content = content; | |
} | |
} | |
/** | |
* Tags by Argument | |
* | |
* Creates a HTML string from the element name, attributes and content given | |
* as part of the arguments provided. | |
* | |
* The name argument can be null, which will produce a standard div element. | |
* If the name is prefixed with a hash, and an id is not provided as an | |
* attribute, then the id attribute will be added. Classes can be described | |
* by using dot notation and will get prefixed to the class attribute. This | |
* familiar syntax follows CSS selector syntax as seen with querySelector | |
* and the jQuery framework. | |
* | |
* Examples of name formats: | |
* - (null|undefined|0) | |
* - div | |
* - div.class#id | |
* - div#id | |
* - .class.another.class#id2 | |
* - .container > .inner > h1 | |
* | |
* The attributes must be given as KeyValuePairs (an object), or a string | |
* (see below), otherwise the provided attributes will not be used. | |
* Examples: | |
* {class: 'alert', style: 'color: red', 'data-msg': 'Example Alert'} | |
* 'class="alert" style="color: red" data-msg="Example Alert"' | |
* | |
* If only two parameters are submitted and the value is either a string or | |
* an array, then an assumption will be made that it is content rather than | |
* an argument. This means that string element arguments can only be given | |
* when ALL 3 arguments are present. | |
* | |
* Set react to true so that classes are set as className attributes rather than | |
* class attributes. The programmer must also realise that classes in react must | |
* use className, otherwise the classes will not be merged into className. | |
* | |
* This allows a lot of flexibility: | |
* tag('p > a.test.example#unique', {href: '/'}, 'hello world') | |
* <p><a class="test example" id="unique" href="/">hello world</a></p> | |
* | |
* tag('.container > .inner > header', 'Header 1') | |
* <div class="container"> | |
* <div class="inner"> | |
* <header>Header 1</header> | |
* </div> | |
* </div> | |
* | |
* tag('.container', [ | |
* '<p>Hello World</p>', | |
* tag('a > span', 'An empty link') | |
* ]) | |
* <div class="container"> | |
* <p>Hello World</p> | |
* <a><span>An empty link</span></a> | |
* </div> | |
* | |
* tag('.container', {class: 'inner', style: 'background: red'}) | |
* <div class="container inner" style="background: red"></div> | |
* | |
* @property {string} name The name of the element | |
* @property {object} attr Object containing attributes (KVPs) | |
* @property {string} content Embedded content | |
* @returns {TagObject} Tag Information | |
*/ | |
export function tag(name?:any, attr?:any, content?:any, react?:boolean): iTagObject { | |
// Use the name or default to a div tag | |
name = (name || 'div'); | |
// default react classes to className as false. | |
react = (react || false); | |
// Check whether the input contains invalid characters | |
var validate = name.match(/(?:([\w\d.#-]+)(\(.*\))?[\s>]*)/g); | |
if (!(validate && validate.join('') === name)) { | |
console.warn('Invalid characters present in element syntax:', name); | |
} | |
// Allow shortened arguments, only allow element arguments if an object | |
// is sent, otherwise expect it as content | |
if (arguments.length === 2 && | |
attr && attr !== null && | |
((typeof(attr) === 'object' && attr.constructor.name === 'Array') || | |
typeof(attr) === 'string')) { | |
content = attr; | |
attr = false; | |
} | |
// Allow CSS syntax to provide surrounded elements, this helps with | |
// library 'exhaustion' but can only be used to surround elements which | |
// can later carry many elements. | |
var surrounds:string[] = name.split(/(\s*>\s*)/g); | |
if (surrounds.length > 1) { | |
name = surrounds.pop(); | |
surrounds = surrounds.filter(e => e.indexOf('>') < 0); | |
} else { | |
surrounds = []; | |
} | |
/** | |
* Parse Attributes. | |
* | |
* Parses attributes in string format into an object. | |
* | |
* @param {string} str attributes in string format | |
*/ | |
var parse_attributes = function(str:string) { | |
var result:{[k:string]:any} = {}; | |
if (str && typeof(str) === 'string') { | |
var r = /(?:(?<key>[\w_-]+)=(?<value>"(?:[^"]+)"|'(?:[^']+)'|(?:[\w\d ]+))(?:\s*$)?)/g; | |
var match:any = matchAll(str, r); | |
var pair:any; | |
while ((pair = match.next()) && !pair.done && (pair = pair.value)) { | |
result[pair.groups.key] = pair.groups.value.replace(/(^["']|['"]$)/g, ''); | |
} | |
} | |
return result; | |
}; | |
// Check attribute arguments and only allow object/strings to be given | |
if (!(attr && typeof(attr) === 'object' && attr.constructor.name === 'Object')) { | |
// If we're provided with a string then try to interpret all of the | |
// attributes into an object. This might feel painful but allows us | |
// to easily append classes/ids from the selector and gives us a | |
// standard format. For speed, the developer should avoid using | |
// strings as we have to manually fetch them. They should ideally | |
// prefer using objects in this scenario too. | |
attr = parse_attributes(attr); | |
} | |
var match:any; | |
if ((match = matchAll(name, /\((.*)\)/g).next().value) && match) { | |
// Attributes were passed in the tag | |
name = name.replace(match[0], ''); | |
attr = Object.assign(parse_attributes(match[1]), attr || {}); | |
} | |
var split = name.split(/(#|\.)/); | |
if (split.length > 1) { | |
name = name.replace(/(#|\.)[\w\d-]+/g, '').trim() || 'div'; | |
var prefixed_class = ''; | |
// Multiple classes can be specified | |
var _i:number; | |
while ((_i = split.indexOf('.')) && _i > -1) { | |
prefixed_class += ' ' + (split[_i + 1] || ''); | |
delete split[_i]; | |
} | |
// Prefix the classes | |
if (prefixed_class) { | |
var class_key:string = (react ? 'className' : 'class'); | |
attr[class_key] = (prefixed_class + ' ' + (attr[class_key] || '')).trim(); | |
} | |
// Add an ID if not present (attribute takes precedence) | |
var _id:number; | |
if ((_id = split.indexOf('#')) && _id > 0 && _id < split.length - 1) { | |
if (!attr['id']) { | |
attr['id'] = split[_id + 1]; | |
} | |
} | |
} | |
// Allow content to be a list which can be joined | |
if (content && typeof(content) === 'object' && content.constructor.name === 'Array') { | |
content = content.join(''); | |
} | |
content = content || ''; | |
var result = new TagObject(name, attr, content); | |
// However, surround the element when a CSS syntax heirarchy exists | |
if (surrounds) { | |
var surround:string|undefined; | |
while ((surround = surrounds.pop()) && surround) { | |
result = tag(surround, null, result); | |
} | |
} | |
return result; | |
}; | |
export function tagReact(name?:any, attr?:any, content?:any) : React.ReactElement { | |
var stack = [tag(name, attr, content, true)]; | |
do { | |
var i = stack.length - 1; | |
var item:any = stack[i]; | |
if ( | |
item.content && | |
typeof(item.content) === 'object' && | |
item.content.constructor && | |
item.content.constructor.name === 'TagObject' | |
) { | |
stack.push(item.content); | |
} else { | |
if (item.attributes.class && !item.attributes.className) { | |
item.attributes.className = item.attributes.class; | |
delete item.attributes.class; | |
} | |
var el = React.createElement(item.name, item.attributes, item.content); | |
stack = stack.slice(0, i); | |
i = stack.length - 1; | |
item = stack[i]; | |
item.content = el; | |
} | |
} while (stack.length > 1); | |
var obj = stack[0]; | |
if (item.attributes.class && !item.attributes.className) { | |
item.attributes.className = item.attributes.class; | |
delete item.attributes.class; | |
} | |
return React.createElement(obj.name, obj.attributes, obj.content); | |
}; | |
export function tagHTML(name?:any, attr?:any, content?:any) : string { | |
// These tags must be closed automatically with HTML4 standards | |
var self_closing = [ | |
'area', | |
'base', | |
'br', | |
'col', | |
'embed', | |
'hr', | |
'img', | |
'input', | |
'link', | |
'meta', | |
'param', | |
'source', | |
'track', | |
'wbr', | |
]; | |
var obj:any = tag(name, attr, content); | |
// Join all attributes together. | |
obj.attributes = Object.keys(obj.attributes).map(function(e) { | |
var value = JSON.stringify(obj.attributes[e]); | |
return e + '=' + (value[0] !== '"' ? JSON.stringify(value) : value); | |
}).join(' '); | |
obj.attributes = obj.attributes || ''; | |
// Finally build the element we require | |
var result = '<' + obj.name + (obj.attributes ? ' ' + obj.attributes : ''); | |
result += ( | |
self_closing.indexOf(obj.name) > -1 ? | |
'/>' : | |
'>' + obj.content + '</' + obj.name + '>' | |
); | |
return result; | |
}; | |
/** | |
* Tag w/ Raw Strings | |
* | |
* Creates a HTML string from the element name, attributes and content given | |
* as part of the arguments provided. | |
* | |
* This function works with raw string data only, and is considered the | |
* fastest way to generate a HTML element using a function. | |
* | |
* @param {string} name The name of the element | |
* @param {string} attr Attributes separated with spaces | |
* @param {string} content Embedded content as a string | |
* @returns {string} The built HTML | |
*/ | |
export function tagRaw(name:any, attr?:any, content?:any) : string { | |
name = name || 'div'; | |
attr = attr || ''; | |
content = content || ''; | |
return '<' + name + (attr ? ' ' + attr : '') + '>' + content + '</' + name + '>'; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment