Skip to content

Instantly share code, notes, and snippets.

@Quacky2200
Last active August 29, 2019 17:12
Show Gist options
  • Save Quacky2200/725254c52dfe24a18a87a7e25677acea to your computer and use it in GitHub Desktop.
Save Quacky2200/725254c52dfe24a18a87a7e25677acea to your computer and use it in GitHub Desktop.
Simple JavaScript template/view/HTML building function.
/**
* 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.
*
* 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>
*
* @param {string} name The name of the element
* @param {object} attr Object containing attributes (KVPs)
* @param {string} content Embedded content
* @returns {string} The built HTML
*/
const tag = function tag(name, attr, content) {
// 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',
];
// Use the name or default to a div tag
name = (name || 'div');
// Check whether the input contains invalid characters
if (name.match(/(?:([\w\d\-\.\#]+)(\(.*\))?[\s>]*)/g, '').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 = name.split(/(\s*>\s*)/g);
if (surrounds.length > 1) {
name = surrounds.pop();
surrounds = surrounds.filter(e => e.indexOf('>') < 0);
} else {
surrounds = false;
}
/**
* Parse Attributes.
*
* Parses attributes in string format into an object.
*
* @param {string} str attributes in string format
*/
var parse_attributes = function(str) {
var result = {};
if (str && typeof(str) === 'string') {
var r = /(?:(?<key>[\w_\-]+)=(?<value>"(?:[^"]+)"|'(?:[^']+)'|(?:[\w\d ]+))(?:\s*$)?)/g;
var match = str.matchAll(r);
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);
}
if ((match = name.matchAll(/\((.*)\)/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
while ((_i = split.indexOf('.')) && _i > -1) {
prefixed_class += ' ' + (split[_i + 1] || '');
delete split[_i];
}
// Prefix the classes
if (prefixed_class) {
attr['class'] = (prefixed_class + ' ' + (attr['class'] || '')).trim();
}
// Add an ID if not present (attribute takes precedence)
if ((_id = split.indexOf('#')) && _id > 0 && _id < split.length - 1) {
if (!attr['id']) {
attr['id'] = split[_id + 1];
}
}
}
// Join all attributes together.
attr = Object.keys(attr).map(function(e) {
var value = JSON.stringify(attr[e]);
return e + '=' + (value[0] !== '"' ? JSON.stringify(value) : value);
}).join(' ');
attr = attr || '';
// Allow content to be a list which can be joined
if (content && typeof(content) === 'object' && content.constructor.name === 'Array') {
content = content.join('');
}
content = content || '';
// Finally build the element we require
var result = '<' + name + (attr ? ' ' + attr : '');
result += (
self_closing.indexOf(name) > -1 ?
'/>' :
'>' + content + '</' + name + '>'
);
// However, surround the element when a CSS syntax heirarchy exists
if (surrounds) {
while ((surround = surrounds.pop()) && surround) {
result = tag(surround, null, result);
}
}
return result;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment