Skip to content

Instantly share code, notes, and snippets.

@Quacky2200
Created September 29, 2019 17:51
Show Gist options
  • Save Quacky2200/19aac2fad6d56f148db17c9194a85f66 to your computer and use it in GitHub Desktop.
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.
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