Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Problem on repeat #14

Open
naccio8 opened this issue Oct 26, 2018 · 1 comment
Open

Problem on repeat #14

naccio8 opened this issue Oct 26, 2018 · 1 comment

Comments

@naccio8
Copy link

naccio8 commented Oct 26, 2018

Good morning @ssimpo a great library congratulations!
There is only one big problem that happens if you want to parse a string with the same tag repeated es [[test]] lore impsum [[/ test]] bla bla bla [[test]] dolor sit [[/ test]]
in this case the parse content returns:
bla bla bla [[test]] dolor sit
instead of the expected lore impsum and dolor sit!!

@naccio8
Copy link
Author

naccio8 commented Oct 26, 2018

I saw after this other issue #13 also reported the same problem
I still solved it temporarily by modifying the script like this:

now you must specify if the shorcode is used in single mode or not :

es [[tag]][[/tag]] [[other_tag]][[/other_tag]] [[tag]][[/tag]] in this case don't use single tag [[tag]]

const short = Shortcode({
    start: '[[',
    end: ']]'
});

es {{tag}} {{other_tag}} in this case don't use closed tag {{tag}}{{/tag}}

const repl = Shortcode({
    start: '{{',
    end: '}}',
    single:true
});
'use strict';
const Promise = require('bluebird');
const _ = require('lodash');
const defaultOptions = {start: '[[', end: ']]',single:false};

const xGetAttributes = new RegExp('(\\S+)\\s*=\\s*([\\\'\\"])(.*?)\\2|(\\S+)\\s*=\\s*(\\S+)|([^\\\'^\\"^\\s]+)(?:\\s|$)|([\\\'\\"])(.*?)\\7', 'g');

const xGetTagAttributesText = '{start}.*?\\s(.*?){end}';
const xStartTagContents ='{start}(.*){end}';
const xTagMatch = '{start}.*?{end}';//\[\[([^\[\[]+[^\]\]]+).{2}
const xtagSepare ='{start}[^{end}]+.{2}([^{start}]+)[^{end}]+.{2}';
const xtagSepareContent ='{start}[^{end}]+.{2}([^{start}]+)[^{end}]+.{2}';
const xIsEndTag = '^{start}\/';
const xGetTagName = '{start}(?:\/|)(.*?)(?:\\s|{end})';
const xStart = /\{start\}/g;
const xEnd = /\{end\}/g;

/**
 * @typedef ShortcodeParserFinder
 * Regular expressions object to use in extracting tag and tag-attribute data.
 *
 * @property {RegExp} tagMatch				Expression for extracting a tag.
 * @property {RegExp} isEndTag				Expression to test if a tag is an
 *											end tag
 * @property {RegExp} getTagName			Expression to extract the tag name.
 * @property {Function} getAttributes		Method to extract the attributes in
 *											a given start tag string.
 * @property {RegExp} getStartTagContent	Expression for extracting the
 * 											contents of start tag.
 */


/**
 * Add slashes to every character in a string.  Can be used to ensure all of
 * contents is treated as text and not used as regular expression functionality
 * when creating a RegExp with the given content.
 *
 * @private
 * @param {string} txt		The string to add slashes to.
 * @returns {string}		New slashed string.
 */
function _addSlashToEachCharacter(txt) {
	return txt.split('').map(char=>'\\' + char).join('');
}

/**
 * Get the attributes in the given tag text. Will return an object of the tag
 * attributes with properties being equal to their names and property values
 * equalling their value. Also, assign numbered properties for attribute
 * positions.
 *
 * @private
 * @param {RegExp} getAttributes        The regular expression to use in getting
 *										the attributes.
 * @param {string} tag					The tag text from open tag start
 *										and close.
 * @returns {Object}					The attributes object.
 */
function _getAttribute(getAttributes, tag) {
	let results = getAttributes.exec(tag);

	let attributes = {};
	if (results) {
		let result;
		let count = 1;
		while (result = xGetAttributes.exec(results[1])) {
			if (!result[6] && (result[1] || result[4])) {
				attributes[count] = {};
				attributes[result[1] || result[4]] = result[3] || result[5];
				attributes[count][result[1] || result[4]] = result[3] || result[5];
			} else if (result[6] || result[8]) {
				attributes[count] = result[6] || result[8];
			}
			count++;
		}
	}

	return attributes;
}

/**
 * Safely create a regular expression from the given template with the given
 * start and end characters replaced in the regular expression.
 *
 * @private
 * @param {string} template			The regular expression template. The text
 *									{start} and {end} will be replaced with
 *									the given startChars and endChars.
 * @param {string} startChars		Tag start characters.
 * @param {string} endChars			Tag end characters.
 * @param {string} [options='']		The regular expression options to
 *									use (eg. 'g' or 'gi').
 * @returns {RegExp}
 */
function _createRegExp(template, startChars, endChars, options = '') {
	return new RegExp(template
		.replace(xStart, _addSlashToEachCharacter(startChars))
		.replace(xEnd, _addSlashToEachCharacter(endChars)
		), options);
}

/**
 * Get an object containing the regular expressions to use in extracting tag
 * and tag-attribute data. Construct these expressions to work with the given
 * start and end tag characters supplied in the options object.
 *
 * @private
 * @class ShortcodeParserFinder
 * @param {object} options				The options object.
 * @param {string} options.start		Start of tag characters.
 * @param {string} options.end			End of tag characters.
 * @returns {ShortcodeParserFinder}
 *
 */
function _createRegExpsObj(options) {
	return {
		single:options.single,
		tagMatch: _createRegExp(xTagMatch, options.start, options.end, 'g'),
		tagSepare: _createRegExp(xtagSepare, options.start, options.end, 'g'),
		tagSepareContent: _createRegExp(xtagSepareContent, options.start, options.end),
		isEndTag: _createRegExp(xIsEndTag, options.start, options.end),
		getTagName: _createRegExp(xGetTagName, options.start, options.end),
		getAttributes: _getAttribute.bind({}, _createRegExp(xGetTagAttributesText, options.start, options.end)),
		getStartTagContent: _createRegExp(xStartTagContents, options.start, options.end)
	};
}

/**
 * Given an array of tags, remove end tags combining them with their start tag
 * and placing tag content in the tag object.
 *
 * @private
 * @param {string} txt					The text containing all the given tags.
 * @param {Array} tags					Array of tag objects.
 * @returns {ShortcodeParserTag[]}		New array with end tags removed and tag
 *										data updated with any tag content.
 */
function _fixEndTags(txt, tags) {
	return tags.map((result, n)=> {
		if (result.endTag) {
			for (let nn = n; nn >= 0; nn--) {
				if ((tags[nn].tagName === result.tagName) && (!tags[nn].endTag)) {
					tags[nn].content = txt.substring(tags[nn].end, result.start);
					tags[nn].fullMatch += (tags[nn].content + result.fullMatch);
					tags[nn].end = result.end;
					tags[nn].selfClosing = false;
				}
			}
		}
		return result;
	}).filter(result=>!result.endTag);
}

/**
 * @private
 * Fix overlapping tags, removing tags inside tags
 *
 * @param {ShortcodeParserTag[]} tags	Tags to filter.
 * @returns {ShortcodeParserTag[]}		Filtered tags.
 */
function _filterOverlappingTags(tags) {
	return tags.filter((tag, n)=> {
		if (n > 0) {
			for (let nn = (n - 1); nn >= 0; nn--) {
				if (tags[nn].end > tag.start) return false;
			}
		}
		return true;
	});
}

/**
 * @typedef ShortcodeParserTag.
 * @property {string} tagName			The name of tag.
 * @property {boolean} endTag			Is this an end tag.
 * @property {string} fullMatch			The full tag text and content.
 * @property {integer} start			Start character number in
 * 										original text.
 * @property {integer} end				end character number in
 *										original text.
 * @property {object} attributes		The tag attributes as an object.
 * @property {string} content			The content of tag when their is
 *										an opening and closing tag.
 * @property {boolean} selfClosing		Is this a self-closing tag?
 * @property {string} tagContents		Contents of starting tag.
 */

/**
 * Create new tag object, describing extracted tag.
 *
 * @private
 * @class ShortcodeParserTag
 * @param {ShortcodeParserFinder} finder	Finder object to apply.
 * @param {Array} result					Results of tag extraction.
 * @returns {ShortcodeParserTag}			New tag object.
 */
function ShortcodeParserTag(finder, result) {
	console.log(finder, result);
	let content='';
	if(!finder.single){
		content=finder.tagSepareContent.exec(result[0])[1];
		console.log(content);
	}
	return {
		tagName: finder.getTagName.exec(result[0])[1],
		endTag: finder.isEndTag.test(result[0]),
		fullMatch: result[0],
		end: result.lastIndex,
		start: result.lastIndex - result[0].length,
		attributes: finder.getAttributes(result[0]),
		content: content,
		tagContents: finder.getStartTagContent.exec(result[0])[1],
		selfClosing: true
	};
}

/**
 * Extract tag strings from given text, return regular expression matches
 * (with some addtional data, such as lastIndex).
 *
 * @private
 * @param {string} txt						Text to extract tags from.
 * @param {ShortcodeParserFinder} finder	Finder object to apply.
 * @returns {Array}							Results array.
 */
function _extractTagStrings(txt, finder) {
	let results = [];
	let result;
	if(finder.single){
		console.log('signle',txt)
		while (result = finder.tagMatch.exec(txt)) {
			result.lastIndex = finder.tagMatch.lastIndex;
			results.push(result);
		}
	}else{
		console.log('no signle')
		while (result = finder.tagSepare.exec(txt)) {
			result.lastIndex = finder.tagSepare.lastIndex;
			results.push(result);
		}
	}
	return results;
}

/**
 * Parse string for tags that handlers have been added for. Return tags that
 * can be parsed.
 *
 * @private
 * @param {string} txt						Text to parse for tags.
 * @param {ShortcodeParserFinder} finder	Finder object to apply.
 * @param {ShortcodeParser} parserInstance	The parser instance.
 * @returns {ShortcodeParserTag[]}			Tags which can be handled.
 */
function _parse(txt, finder, parserInstance) {
	let test=_extractTagStrings(txt, finder).map(
		result=>ShortcodeParserTag(finder, result)
	);
	console.log(test)
	return test;
}

/**
 *  @typedef ShortcodeParserReplacer
 *  @property {string} replacer				Text to replace tag with.
 *  @property {ShortcodeParserTag} tag		Tag to do replacement on.
 */

/**
 * Apply a handler function to a given tag with supplied parameters.
 *
 * @private
 * @param {Function} handler						Handler to apply.
 * @param {ShortcodeParserTag} tag					Tag to apply handler to.
 * @param {Array} params							Further parameters to pass
 * 													to the handler.
 * @returns {Promise.<ShortcodeParserReplacer>}
 */
function _applyHandler(handler, tag, params) {
	return Promise.resolve(handler.apply({}, params) || '').then(replacer=>{
		return {replacer, tag};
	});
}

/**
 * Test if given selector is selector for the given tag.
 *
 * @private
 * @param {RegExp|Function} selector		Selector to test.
 * @param {ShortcodeParserTag} tag			Tag to test against.
 * @returns {boolean}
 */
function _isSelectorMatch(selector, tag) {
	return ((_.isRegExp(selector) && selector.test(tag.tagContents)) || (_.isFunction(selector) && selector(tag.tagContents)));
}

/**
 * Create a new Shortcode parser instance.
 *
 * @class
 * @public
 * @param {Object} options			Options to ShortcodeParser function.
 * @returns {ShortcodeParser}		New instance of shortcode parser.
 */
function ShortcodeParser(options = defaultOptions) {
	const tags = new Map();
	const finder = _createRegExpsObj(Object.assign({}, defaultOptions, options));

	/**
	 * Run set handlers for given tags, replacing text content as the handler
	 * return content.
	 *
	 * @private
	 * @param {string} txt						The full text containing the tags to
	 *											do the replacements on.
	 * @param {ShortcodeParserTag[]} _tags		The tags to run handlers on.
	 * @param {Array} params					The parameters to pass on to the
	 *											tag handlers.
	 * @returns {Promise.<string>}				Promise resolving on completion of
	 *											tag replacements.
	 */
	function _runHandlers(txt, _tags, params) {
		return Promise.all(_tags.map(tag=>{
			let promise;
			if (exports.has(tag.tagName)) {
				let handler = exports.get(tag.tagName).bind({}, tag);
				promise = _applyHandler(handler, tag, params);
			} else {
				tags.forEach((handler, selector)=>{
					let _handler = handler.bind({}, tag);
					if (!promise && _isSelectorMatch(selector, tag)) promise = _applyHandler(_handler, tag, params);
				});
			}
			return promise;
		})).filter(
			result=>result
		).mapSeries(result=>{
			txt = txt.replace(result.tag.fullMatch, result.replacer);
		}).then(()=>txt);
	}

	const exports = {
		/**
		 * Add a new handler to the parser for given tag name.
		 *
		 * @public
		 * @memberof ShortcodeParser
		 * @param {string|Function|RegExp} name			Tag name to set handler for.
		 * @param {function} handler					Handler function to fire on tag.
		 * @param {boolean} [throwOnAlreadySet=true]	Throw error if tage already exists?
		 * @return {function}							The handler function returned.
		 */
		add: (name, handler, throwOnAlreadySet=true)=> {
			if (exports.has(name) && throwOnAlreadySet) throw new Error(`Tag '${name}' already exists`);
			if (!_.isFunction(handler)) throw new TypeError(`Cannot assign a non function as handler method for '${name}'`);
			if (!_.isString(name) && !_.isRegExp(name) && !_.isFunction(name)) throw new TypeError('Cannot add handler if the reference is not a string, regular expression or function. Reference of type: ' + (typeof name) + ', was given.');
			tags.set(name, handler);
			return exports.get(name);
		},

		/**
		 * Test if a handler for given tag name.
		 *
		 * @public
		 * @memberof ShortcodeParser
		 * @param {string} name			The tag to look for a handler on.
		 * @returns {boolean}			Does it exist?
		 */
		has: name=>tags.has(name),

		/**
		 * Delete the handler for given tag name.
		 *
		 * @public
		 * @memberof ShortcodeParser
		 * @param {string} name			The tagname to delete the handler for.
		 * @returns {boolean}
		 */
		delete: name=> {
			if (!exports.has(name)) throw new RangeError(`Tag '${name}' does not exist`);
			return tags.delete(name);
		},

		/**
		 * Get the handler function for given tag name.
		 *
		 * @public
		 * @memberof ShortcodeParser
		 * @param {string} name			Tag name to get the handler for.
		 * @returns {function}			The handler for the given tag name.
		 */
		get: name=> {
			if (!exports.has(name)) throw new RangeError(`Tag '${name}' does not exist`);
			return tags.get(name);
		},

		/**
		 * Parse given text for tags, running handlers where handlers are
		 * defined and returning parsed text.
		 *
		 * @public
		 * @memberof ShortcodeParser
		 * @param {string} txt				Text to parse.
		 * @param {Array} [params=[]]		Parameters to pass to the handlers.
		 * @returns {Promise.<string>}		Promise resolving to new
		 *									parsed text.
		 */
		parse: (txt, ...params)=> {
			let tags = _filterOverlappingTags(_fixEndTags(txt, _parse(txt, finder, exports)));

			return _runHandlers(txt, tags, params).then(parsedTxt=> {
				if (txt !== parsedTxt) return exports.parse(parsedTxt, finder, exports);
				return parsedTxt;
			});
		}
	};

	return Object.freeze(exports);
}


module.exports = ShortcodeParser;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant