Source: src/app/addons/remarkable.utils.js

/**
 * MarkdownMaster CMS
 *
 * The MIT License (MIT)
 * Copyright (c) 2023 Charlie Powell
 * https://github.com/cdp1337/markdownmaster
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
 * associated documentation files (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge, publish, distribute,
 * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
 * is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies
 * or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
 * AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

/**
 * Copy of functions from the remarkable source code to support custom functionality
 */

/*
var utils = Object.freeze({
	isString: isString,
	has: has$1,
	assign: assign,
	unescapeMd: unescapeMd,
	isValidEntityCode: isValidEntityCode,
	fromCodePoint: fromCodePoint,
	replaceEntities: replaceEntities,
	escapeHtml: escapeHtml
});
*/
import {utils} from 'remarkable';
import {AttributeBuilder} from '../utils';

export const CHAR = {
	LF         : 0x0A,
	SPACE      : 0x20,
	EXCLAIM    : 0x21,
	DBLQUOTE   : 0x22,
	POUND      : 0x23,
	SNGQUOTE   : 0x27,
	OPNPARAN   : 0x28,
	CLSPARAN   : 0x29,
	SLASH      : 0x2F,
	LESSTHAN   : 0x3C,
	EQUAL      : 0x3D,
	GREATERTHAN: 0x3E,
	QUESTION   : 0x3F,
	OPNBRACKET : 0x5B,
	BACKSLASH  : 0x5C,
	CLSBRACKET : 0x5D,
	a          : 0x61,
	z          : 0x7A,
	OPNBRACE   : 0x7B,
	CLSBRACE   : 0x7D,
	DEL        : 0x7F
};

/**
 * Parse link labels
 *
 * This function assumes that first character (`[`) already matches;
 * returns the end of the label.
 *
 * @param  {Object} state
 * @param  {Number} start
 * @api private
 */
export function parseLinkLabel(state, start) {
	let level, found, marker,
		labelEnd = -1,
		max = state.posMax,
		oldPos = state.pos,
		oldFlag = state.isInLabel;

	if (state.isInLabel) { return -1; }

	if (state.labelUnmatchedScopes) {
		state.labelUnmatchedScopes--;
		return -1;
	}

	state.pos = start + 1;
	state.isInLabel = true;
	level = 1;

	while (state.pos < max) {
		marker = state.src.charCodeAt(state.pos);
		if (marker === CHAR.OPNBRACKET /* [ */) {
			level++;
		} else if (marker === CHAR.CLSBRACKET /* ] */) {
			level--;
			if (level === 0) {
				found = true;
				break;
			}
		}

		state.parser.skipToken(state);
	}

	if (found) {
		labelEnd = state.pos;
		state.labelUnmatchedScopes = 0;
	} else {
		state.labelUnmatchedScopes = level - 1;
	}

	// restore old state
	state.pos = oldPos;
	state.isInLabel = oldFlag;

	return labelEnd;
}

/**
 * Parse link destination
 *
 *   - on success it returns a string and updates state.pos;
 *   - on failure it returns null
 *
 * @param  {Object} state
 * @param  {Number} pos
 * @api private
 */
export function parseLinkDestination(state, pos) {
	let code, level, link,
		start = pos,
		max = state.posMax;

	if (state.src.charCodeAt(pos) === CHAR.LESSTHAN /* < */) {
		pos++;
		while (pos < max) {
			code = state.src.charCodeAt(pos);
			if (code === CHAR.LF /* \n */) { return false; }
			if (code === CHAR.GREATERTHAN /* > */) {
				link = normalizeLink(utils.unescapeMd(state.src.slice(start + 1, pos)));
				if (!state.parser.validateLink(link)) { return false; }
				state.pos = pos + 1;
				state.linkContent = link;
				return true;
			}
			if (code === CHAR.BACKSLASH /* \ */ && pos + 1 < max) {
				pos += 2;
				continue;
			}

			pos++;
		}

		// no closing '>'
		return false;
	}

	// this should be ... } else { ... branch

	level = 0;
	while (pos < max) {
		code = state.src.charCodeAt(pos);

		if (code === CHAR.SPACE) { break; }

		// ascii control chars
		if (code < CHAR.SPACE || code === CHAR.DEL) { break; }

		if (code === CHAR.BACKSLASH /* \ */ && pos + 1 < max) {
			pos += 2;
			continue;
		}

		if (code === CHAR.OPNPARAN /* ( */) {
			level++;
			if (level > 1) { break; }
		}

		if (code === CHAR.CLSPARAN /* ) */) {
			level--;
			if (level < 0) { break; }
		}

		pos++;
	}

	if (start === pos) { return false; }

	link = utils.unescapeMd(state.src.slice(start, pos));
	if (!state.parser.validateLink(link)) { return false; }

	state.linkContent = link;
	state.pos = pos;
	return true;
}

function normalizeLink(url) {
	let normalized = utils.replaceEntities(url);
	// We shouldn't care about the result of malformed URIs,
	// and should not throw an exception.
	try {
		normalized = decodeURI(normalized);
	} catch (err) {
		console.log(err, {normalized});
	}
	return encodeURI(normalized);
}

/**
 * Parse link title
 *
 *   - on success it returns a string and updates state.pos;
 *   - on failure it returns null
 *
 * @param  {Object} state
 * @param  {Number} pos
 * @api private
 */
export function parseLinkTitle(state, pos) {
	let code,
		start = pos,
		max = state.posMax,
		marker = state.src.charCodeAt(pos);

	if (marker !== CHAR.DBLQUOTE /* " */ && marker !== CHAR.SNGQUOTE /* ' */ && marker !== CHAR.OPNPARAN /* ( */) { return false; }

	pos++;

	// if opening marker is "(", switch it to closing marker ")"
	if (marker === CHAR.OPNPARAN) { marker = CHAR.CLSPARAN; }

	while (pos < max) {
		code = state.src.charCodeAt(pos);
		if (code === marker) {
			state.pos = pos + 1;
			state.linkContent = utils.unescapeMd(state.src.slice(start + 1, pos));
			return true;
		}
		if (code === CHAR.BACKSLASH /* \ */ && pos + 1 < max) {
			pos += 2;
			continue;
		}

		pos++;
	}

	return false;
}

/**
 * Parse extended HTML attributes for nodes
 *
 * ie: {.classname #some_id att=value att2=another}
 *
 *   - on success it returns a string and updates state.pos;
 *   - on failure it returns null
 *
 * @param  {Object} state
 * @param  {Number} pos
 * @api private
 */
export function parseExtendedAttributes(state, pos) {
	let code,
		start = pos,
		max = state.posMax,
		marker = state.src.charCodeAt(pos);

	if (marker !== CHAR.OPNBRACE) { return false; }

	pos++;

	while (pos < max) {
		code = state.src.charCodeAt(pos);
		if (code === CHAR.CLSBRACE) {
			state.pos = pos + 1;
			state.attributes = new AttributeBuilder(state.src.slice(start + 1, pos));
			return true;
		}
		if (code === CHAR.BACKSLASH && pos + 1 < max) {
			// Skip backslash characters (to allow escaping braces)
			pos += 2;
			continue;
		}

		pos++;
	}

	return false;
}

export function normalizeReference(str) {
	// use .toUpperCase() instead of .toLowerCase()
	// here to avoid a conflict with Object.prototype
	// members (most notably, `__proto__`)
	return str.trim().replace(/\s+/g, ' ').toUpperCase();
}