react-marksome
Advanced tools
Comparing version 0.3.0 to 1.0.0
@@ -10,27 +10,16 @@ 'use strict'; | ||
const STRONG_TEXT_REGEXP = /([*_])\1\1?((?:\[.*?\][([].*?[)\]]|.)*?)\1?\1\1/g; | ||
const EMPHASIZED_TEXT_REGEXP = /([*_])((?:\[.*?\][([].*?[)\]]|.)*?)\1/g; | ||
const REFERENCE_LINK_TEXT_REGEXP = /\[([^\]]*)\] ?\[([^\]]*)\]/g; | ||
// Capture [text][reference] or [reference] (taking into account escaped squared brackets) | ||
const REFERENCE_LINK_REGEXP = /(?<!\\)(?:\\\\)*(?:\[(.+?)(?<!\\)(?:\\\\)*\])?\[((?:(?<!\\)(?:\\\\)*\\[[]|[^[])+?)(?<!\\)(?:\\\\)*\]/g; // Capture sequence of '*' or '_' (non-escaped) | ||
function matchAll(regexp, text, onMatch) { | ||
let match; | ||
while ((match = regexp.exec(text)) !== null) { | ||
onMatch(match); | ||
} | ||
} | ||
const EMPH_SEQUENCE_REGEXP = /(?<!\\)(?:\\\\)*(\*+|_+)/g; | ||
function parseSegments(text) { | ||
const matches = []; | ||
matchAll(REFERENCE_LINK_TEXT_REGEXP, text, referenceLinkRegExpMatch => { | ||
const innerText = referenceLinkRegExpMatch[1]; | ||
const pendingOpenersByBlockIndex = new Map([[-1, []]]); | ||
const referenceLinkMatches = []; | ||
matchAll(REFERENCE_LINK_REGEXP, text, referenceLinkRegExpMatch => { | ||
const reference = referenceLinkRegExpMatch[2]; | ||
const startIndex = referenceLinkRegExpMatch.index; | ||
if (!innerText || !reference || startIndex == null) { | ||
return; | ||
} | ||
const innerText = referenceLinkRegExpMatch[1] || reference; | ||
const endIndex = startIndex + referenceLinkRegExpMatch[0].length; | ||
matches.push({ | ||
const match = { | ||
type: 'reference-link', | ||
@@ -42,17 +31,115 @@ innerText, | ||
offset: 1 | ||
}); | ||
}; | ||
pendingOpenersByBlockIndex.set(referenceLinkMatches.length, []); | ||
referenceLinkMatches.push(match); | ||
matches.push(match); | ||
}); | ||
matchAll(STRONG_TEXT_REGEXP, text, strongRegExpMatch => { | ||
const inlineMatch = getInlineMatchFromRegexpMatch(strongRegExpMatch, 'strong'); | ||
let currentReferenceLinkIndex = 0; | ||
matchAll(EMPH_SEQUENCE_REGEXP, text, emphCharRegExpMatch => { | ||
const char = emphCharRegExpMatch[1][0]; | ||
const length = emphCharRegExpMatch[0].length; | ||
const index = emphCharRegExpMatch.index; | ||
const previousCharInfo = getCharInfo(text[index - 1]); | ||
const nextCharInfo = getCharInfo(text[index + length]); | ||
const leftFlanking = !nextCharInfo || nextCharInfo === '.' && !!previousCharInfo; | ||
const rightFlanking = !previousCharInfo || previousCharInfo === '.' && !!nextCharInfo; | ||
let canOpen = leftFlanking; | ||
let canClose = rightFlanking; | ||
if (inlineMatch) { | ||
matches.push(inlineMatch); | ||
if (char === '_') { | ||
canOpen = leftFlanking && (!rightFlanking || previousCharInfo === '.'); | ||
canClose = rightFlanking && (!leftFlanking || nextCharInfo === '.'); | ||
} | ||
}); | ||
matchAll(EMPHASIZED_TEXT_REGEXP, text, emphasisRegExpMatch => { | ||
const inlineMatch = getInlineMatchFromRegexpMatch(emphasisRegExpMatch, 'emphasis'); | ||
if (inlineMatch) { | ||
matches.push(inlineMatch); | ||
if (!canOpen && !canClose) { | ||
return; | ||
} // identify current delimiter block index | ||
let blockIndex = -1; | ||
for (; currentReferenceLinkIndex < referenceLinkMatches.length; currentReferenceLinkIndex++) { | ||
const currentReferenceLinkMatch = referenceLinkMatches[currentReferenceLinkIndex]; // comes before the current block -> it's outside a block | ||
if (currentReferenceLinkMatch.startIndex > index) { | ||
break; | ||
} // comes after the current block -> check next block | ||
if (currentReferenceLinkMatch.endIndex <= index) { | ||
continue; | ||
} // it's inside the block but outside it's innerText -> ignore this emph char match | ||
if (currentReferenceLinkMatch.startIndex + currentReferenceLinkMatch.offset + currentReferenceLinkMatch.innerText.length < index) { | ||
currentReferenceLinkIndex++; | ||
return; | ||
} | ||
blockIndex = currentReferenceLinkIndex; | ||
break; | ||
} | ||
const delimiter = { | ||
char, | ||
index, | ||
length, | ||
type: canOpen && canClose ? '<>' : canClose ? '>' : '<' | ||
}; | ||
const pendingOpeners = pendingOpenersByBlockIndex.get(blockIndex); // can close -> look for last compatible opener | ||
if (delimiter.type !== '<') { | ||
for (let pendingOpenerIndex = pendingOpeners.length - 1; pendingOpenerIndex >= 0; pendingOpenerIndex--) { | ||
const pendingOpener = pendingOpeners[pendingOpenerIndex]; // ensure that the pendingOpener is the same character | ||
if (pendingOpener.char !== delimiter.char) { | ||
continue; | ||
} // from spec (https://spec.commonmark.org/0.30/#emphasis-and-strong-emphasis rule 9) | ||
// If one of the delimiters can both open and close emphasis, | ||
// then the sum of the lengths of the delimiter runs containing the opening and closing delimiters | ||
// must not be a multiple of 3 unless both lengths are multiples of 3 | ||
if (pendingOpener.type === '<>' || delimiter.type === '<>') { | ||
if ((pendingOpener.length + delimiter.length) % 3 === 0) { | ||
if (pendingOpener.length % 3 || delimiter.length % 3) { | ||
continue; | ||
} | ||
} | ||
} // it's a match! | ||
delimiter.type = '>'; | ||
let matchDelimiterLength = Math.min(delimiter.length, pendingOpener.length); // for each pair -> extract strong based on matchDelimiterInnerOffset | ||
while (matchDelimiterLength > 1) { | ||
matches.push(createInlineStyleMatch(text, 'strong', pendingOpener, delimiter)); | ||
matchDelimiterLength -= 2; | ||
} // if one left -> extract emphasis based on matchDelimiterInnerOffset | ||
if (matchDelimiterLength) { | ||
matches.push(createInlineStyleMatch(text, 'emphasis', pendingOpener, delimiter)); | ||
} // if opener is wider than closer | ||
if (pendingOpener.length) { | ||
// remove openers until current one (exclusive) | ||
pendingOpeners.splice(pendingOpenerIndex + 1); | ||
} else { | ||
// remove openers until current one (inclusive) | ||
pendingOpeners.splice(pendingOpenerIndex); // if closer is wider than opener -> look for more openers | ||
if (delimiter.length) { | ||
continue; | ||
} | ||
} | ||
break; | ||
} | ||
} | ||
if (delimiter.type !== '>') { | ||
pendingOpeners.push(delimiter); | ||
} | ||
}); | ||
@@ -63,18 +150,23 @@ matches.sort((a, b) => a.startIndex - b.startIndex); | ||
function getInlineMatchFromRegexpMatch(regexpMatch, inlineType) { | ||
const startIndex = regexpMatch.index; | ||
const innerText = regexpMatch[2]; | ||
function matchAll(regexp, text, onMatch) { | ||
let match; | ||
if (startIndex == null || !innerText) { | ||
return; | ||
while ((match = regexp.exec(text)) !== null) { | ||
onMatch(match); | ||
} | ||
} | ||
const decoratedText = regexpMatch[0]; | ||
const offset = decoratedText.indexOf(innerText); | ||
const endIndex = startIndex + decoratedText.length; | ||
function createInlineStyleMatch(text, type, opener, closer) { | ||
const innerTextStartIndex = opener.index + opener.length; | ||
const innerTextEndIndex = closer.index; | ||
const offset = type === 'strong' ? 2 : 1; // adjust delimiters | ||
opener.length -= offset; | ||
closer.length -= offset; | ||
closer.index += offset; | ||
return { | ||
type: inlineType, | ||
startIndex, | ||
endIndex, | ||
innerText, | ||
type, | ||
startIndex: innerTextStartIndex - offset, | ||
endIndex: innerTextEndIndex + offset, | ||
innerText: text.slice(innerTextStartIndex, innerTextEndIndex), | ||
offset | ||
@@ -84,9 +176,23 @@ }; | ||
function getCharInfo(char) { | ||
// detect spaces | ||
if (!char || /\s/.exec(char)) { | ||
return ' '; | ||
} // detect punctuation | ||
if (/[!"#$%&'()*+,.\/:;<=>?@[\\\]^_`{|}~-]/.exec(char)) { | ||
return '.'; | ||
} | ||
return; | ||
} | ||
function getSegmentsFromMatches(text, matches) { | ||
if (!matches.length) { | ||
return [text]; | ||
return [unescapeText(text)]; | ||
} | ||
const firstMatchStartIndex = matches[0].startIndex; | ||
const segments = firstMatchStartIndex > 0 ? [text.slice(0, firstMatchStartIndex)] : []; | ||
const segments = firstMatchStartIndex > 0 ? [unescapeText(text.slice(0, firstMatchStartIndex))] : []; | ||
@@ -96,14 +202,6 @@ while (matches.length) { | ||
const currentMatchTextStart = currentMatch.startIndex + currentMatch.offset; | ||
const innerMatches = []; | ||
const innerMatches = []; // find innerMatches | ||
for (let i = 0; i < matches.length;) { | ||
const otherMatch = matches[i]; // if not an inner match, continue to the next | ||
if (otherMatch.endIndex > currentMatch.endIndex) { | ||
i++; | ||
continue; | ||
} // remove it from matches | ||
matches.splice(i, 1); | ||
while (matches.length && matches[0].endIndex < currentMatch.endIndex) { | ||
const otherMatch = matches.shift(); | ||
otherMatch.startIndex -= currentMatchTextStart; | ||
@@ -132,3 +230,3 @@ otherMatch.endIndex -= currentMatchTextStart; | ||
if (textAfterLastMatch) { | ||
segments.push(textAfterLastMatch); | ||
segments.push(unescapeText(textAfterLastMatch)); | ||
} | ||
@@ -140,2 +238,7 @@ } | ||
function unescapeText(text) { | ||
// subset of escapable markdown chars which are used as markers in this lib | ||
return text.replace(/\\([*[\\\]_])/g, '$1'); | ||
} | ||
function Marksome({ | ||
@@ -142,0 +245,0 @@ text, |
@@ -1,2 +0,2 @@ | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e,n=require("react"),t=(e=n)&&"object"==typeof e&&"default"in e?e.default:e;const r=/([*_])\1\1?((?:\[.*?\][([].*?[)\]]|.)*?)\1?\1\1/g,s=/([*_])((?:\[.*?\][([].*?[)\]]|.)*?)\1/g,c=/\[([^\]]*)\] ?\[([^\]]*)\]/g;function o(e,n,t){let r;for(;null!==(r=e.exec(n));)t(r)}function u(e){const n=[];return o(c,e,e=>{const t=e[1],r=e[2],s=e.index;t&&r&&null!=s&&n.push({type:"reference-link",innerText:t,reference:r,startIndex:s,endIndex:s+e[0].length,offset:1})}),o(r,e,e=>{const t=f(e,"strong");t&&n.push(t)}),o(s,e,e=>{const t=f(e,"emphasis");t&&n.push(t)}),n.sort((e,n)=>e.startIndex-n.startIndex),function e(n,t){if(!t.length)return[n];const r=t[0].startIndex,s=r>0?[n.slice(0,r)]:[];for(;t.length;){const r=t.shift(),c=r.startIndex+r.offset,o=[];for(let e=0;e<t.length;){const n=t[e];n.endIndex>r.endIndex?e++:(t.splice(e,1),n.startIndex-=c,n.endIndex-=c,o.push(n))}const u=e(r.innerText,o);s.push("reference-link"===r.type?{type:r.type,content:u,reference:r.reference}:{type:r.type,content:u});const f=t.length?n.slice(r.endIndex,t[0].startIndex):n.slice(r.endIndex);f&&s.push(f)}return s}(e,n)}function f(e,n){const t=e.index,r=e[2];if(null==t||!r)return;const s=e[0],c=s.indexOf(r);return{type:n,startIndex:t,endIndex:t+s.length,innerText:r,offset:c}}exports.Marksome=function({text:e,references:r,...s}){const c=n.useMemo(()=>u(e),[e]);return t.createElement("span",Object.assign({},s),function e(r,s){return r.map((r,c)=>{if("string"==typeof r)return r;switch(r.type){case"strong":return t.createElement("strong",{key:c},e(r.content,s));case"emphasis":return t.createElement("em",{key:c},e(r.content,s));case"reference-link":{const o=null==s?void 0:s[r.reference],u=e(r.content,s);if(!o)return t.createElement("span",{key:c},u);if("string"==typeof o)return t.createElement("a",{key:c,href:o},u);const f=o(u);return n.cloneElement(f,{key:c})}default:return null}})}(c,r))},exports.parseSegments=u; | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e,t=require("react"),n=(e=t)&&"object"==typeof e&&"default"in e?e.default:e;const r=/(?<!\\)(?:\\\\)*(?:\[(.+?)(?<!\\)(?:\\\\)*\])?\[((?:(?<!\\)(?:\\\\)*\\[[]|[^[])+?)(?<!\\)(?:\\\\)*\]/g,s=/(?<!\\)(?:\\\\)*(\*+|_+)/g;function c(e){const t=[],n=new Map([[-1,[]]]),c=[];o(r,e,e=>{const r=e[2],s=e.index,o={type:"reference-link",innerText:e[1]||r,reference:r,startIndex:s,endIndex:s+e[0].length,offset:1};n.set(c.length,[]),c.push(o),t.push(o)});let u=0;return o(s,e,r=>{const s=r[1][0],o=r[0].length,f=r.index,a=l(e[f-1]),d=l(e[f+o]),h=!d||"."===d&&!!a,p=!a||"."===a&&!!d;let x=h,g=p;if("_"===s&&(x=h&&(!p||"."===a),g=p&&(!h||"."===d)),!x&&!g)return;let y=-1;for(;u<c.length;u++){const e=c[u];if(e.startIndex>f)break;if(!(e.endIndex<=f)){if(e.startIndex+e.offset+e.innerText.length<f)return void u++;y=u;break}}const I={char:s,index:f,length:o,type:x&&g?"<>":g?">":"<"},m=n.get(y);if("<"!==I.type)for(let n=m.length-1;n>=0;n--){const r=m[n];if(r.char!==I.char)continue;if(("<>"===r.type||"<>"===I.type)&&(r.length+I.length)%3==0&&(r.length%3||I.length%3))continue;I.type=">";let s=Math.min(I.length,r.length);for(;s>1;)t.push(i(e,"strong",r,I)),s-=2;if(s&&t.push(i(e,"emphasis",r,I)),r.length)m.splice(n+1);else if(m.splice(n),I.length)continue;break}">"!==I.type&&m.push(I)}),t.sort((e,t)=>e.startIndex-t.startIndex),function e(t,n){if(!n.length)return[f(t)];const r=n[0].startIndex,s=r>0?[f(t.slice(0,r))]:[];for(;n.length;){const r=n.shift(),c=r.startIndex+r.offset,o=[];for(;n.length&&n[0].endIndex<r.endIndex;){const e=n.shift();e.startIndex-=c,e.endIndex-=c,o.push(e)}const i=e(r.innerText,o);s.push("reference-link"===r.type?{type:r.type,content:i,reference:r.reference}:{type:r.type,content:i});const l=n.length?t.slice(r.endIndex,n[0].startIndex):t.slice(r.endIndex);l&&s.push(f(l))}return s}(e,t)}function o(e,t,n){let r;for(;null!==(r=e.exec(t));)n(r)}function i(e,t,n,r){const s=n.index+n.length,c=r.index,o="strong"===t?2:1;return n.length-=o,r.length-=o,r.index+=o,{type:t,startIndex:s-o,endIndex:c+o,innerText:e.slice(s,c),offset:o}}function l(e){return!e||/\s/.exec(e)?" ":/[!"#$%&'()*+,.\/:;<=>?@[\\\]^_`{|}~-]/.exec(e)?".":void 0}function f(e){return e.replace(/\\([*[\\\]_])/g,"$1")}exports.Marksome=function({text:e,references:r,...s}){const o=t.useMemo(()=>c(e),[e]);return n.createElement("span",Object.assign({},s),function e(r,s){return r.map((r,c)=>{if("string"==typeof r)return r;switch(r.type){case"strong":return n.createElement("strong",{key:c},e(r.content,s));case"emphasis":return n.createElement("em",{key:c},e(r.content,s));case"reference-link":{const o=null==s?void 0:s[r.reference],i=e(r.content,s);if(!o)return n.createElement("span",{key:c},i);if("string"==typeof o)return n.createElement("a",{key:c,href:o},i);const l=o(i);return t.cloneElement(l,{key:c})}default:return null}})}(o,r))},exports.parseSegments=c; | ||
//# sourceMappingURL=react-marksome.cjs.production.min.js.map |
import React, { useMemo, cloneElement } from 'react'; | ||
const STRONG_TEXT_REGEXP = /([*_])\1\1?((?:\[.*?\][([].*?[)\]]|.)*?)\1?\1\1/g; | ||
const EMPHASIZED_TEXT_REGEXP = /([*_])((?:\[.*?\][([].*?[)\]]|.)*?)\1/g; | ||
const REFERENCE_LINK_TEXT_REGEXP = /\[([^\]]*)\] ?\[([^\]]*)\]/g; | ||
// Capture [text][reference] or [reference] (taking into account escaped squared brackets) | ||
const REFERENCE_LINK_REGEXP = /(?<!\\)(?:\\\\)*(?:\[(.+?)(?<!\\)(?:\\\\)*\])?\[((?:(?<!\\)(?:\\\\)*\\[[]|[^[])+?)(?<!\\)(?:\\\\)*\]/g; // Capture sequence of '*' or '_' (non-escaped) | ||
function matchAll(regexp, text, onMatch) { | ||
let match; | ||
while ((match = regexp.exec(text)) !== null) { | ||
onMatch(match); | ||
} | ||
} | ||
const EMPH_SEQUENCE_REGEXP = /(?<!\\)(?:\\\\)*(\*+|_+)/g; | ||
function parseSegments(text) { | ||
const matches = []; | ||
matchAll(REFERENCE_LINK_TEXT_REGEXP, text, referenceLinkRegExpMatch => { | ||
const innerText = referenceLinkRegExpMatch[1]; | ||
const pendingOpenersByBlockIndex = new Map([[-1, []]]); | ||
const referenceLinkMatches = []; | ||
matchAll(REFERENCE_LINK_REGEXP, text, referenceLinkRegExpMatch => { | ||
const reference = referenceLinkRegExpMatch[2]; | ||
const startIndex = referenceLinkRegExpMatch.index; | ||
if (!innerText || !reference || startIndex == null) { | ||
return; | ||
} | ||
const innerText = referenceLinkRegExpMatch[1] || reference; | ||
const endIndex = startIndex + referenceLinkRegExpMatch[0].length; | ||
matches.push({ | ||
const match = { | ||
type: 'reference-link', | ||
@@ -34,17 +23,115 @@ innerText, | ||
offset: 1 | ||
}); | ||
}; | ||
pendingOpenersByBlockIndex.set(referenceLinkMatches.length, []); | ||
referenceLinkMatches.push(match); | ||
matches.push(match); | ||
}); | ||
matchAll(STRONG_TEXT_REGEXP, text, strongRegExpMatch => { | ||
const inlineMatch = getInlineMatchFromRegexpMatch(strongRegExpMatch, 'strong'); | ||
let currentReferenceLinkIndex = 0; | ||
matchAll(EMPH_SEQUENCE_REGEXP, text, emphCharRegExpMatch => { | ||
const char = emphCharRegExpMatch[1][0]; | ||
const length = emphCharRegExpMatch[0].length; | ||
const index = emphCharRegExpMatch.index; | ||
const previousCharInfo = getCharInfo(text[index - 1]); | ||
const nextCharInfo = getCharInfo(text[index + length]); | ||
const leftFlanking = !nextCharInfo || nextCharInfo === '.' && !!previousCharInfo; | ||
const rightFlanking = !previousCharInfo || previousCharInfo === '.' && !!nextCharInfo; | ||
let canOpen = leftFlanking; | ||
let canClose = rightFlanking; | ||
if (inlineMatch) { | ||
matches.push(inlineMatch); | ||
if (char === '_') { | ||
canOpen = leftFlanking && (!rightFlanking || previousCharInfo === '.'); | ||
canClose = rightFlanking && (!leftFlanking || nextCharInfo === '.'); | ||
} | ||
}); | ||
matchAll(EMPHASIZED_TEXT_REGEXP, text, emphasisRegExpMatch => { | ||
const inlineMatch = getInlineMatchFromRegexpMatch(emphasisRegExpMatch, 'emphasis'); | ||
if (inlineMatch) { | ||
matches.push(inlineMatch); | ||
if (!canOpen && !canClose) { | ||
return; | ||
} // identify current delimiter block index | ||
let blockIndex = -1; | ||
for (; currentReferenceLinkIndex < referenceLinkMatches.length; currentReferenceLinkIndex++) { | ||
const currentReferenceLinkMatch = referenceLinkMatches[currentReferenceLinkIndex]; // comes before the current block -> it's outside a block | ||
if (currentReferenceLinkMatch.startIndex > index) { | ||
break; | ||
} // comes after the current block -> check next block | ||
if (currentReferenceLinkMatch.endIndex <= index) { | ||
continue; | ||
} // it's inside the block but outside it's innerText -> ignore this emph char match | ||
if (currentReferenceLinkMatch.startIndex + currentReferenceLinkMatch.offset + currentReferenceLinkMatch.innerText.length < index) { | ||
currentReferenceLinkIndex++; | ||
return; | ||
} | ||
blockIndex = currentReferenceLinkIndex; | ||
break; | ||
} | ||
const delimiter = { | ||
char, | ||
index, | ||
length, | ||
type: canOpen && canClose ? '<>' : canClose ? '>' : '<' | ||
}; | ||
const pendingOpeners = pendingOpenersByBlockIndex.get(blockIndex); // can close -> look for last compatible opener | ||
if (delimiter.type !== '<') { | ||
for (let pendingOpenerIndex = pendingOpeners.length - 1; pendingOpenerIndex >= 0; pendingOpenerIndex--) { | ||
const pendingOpener = pendingOpeners[pendingOpenerIndex]; // ensure that the pendingOpener is the same character | ||
if (pendingOpener.char !== delimiter.char) { | ||
continue; | ||
} // from spec (https://spec.commonmark.org/0.30/#emphasis-and-strong-emphasis rule 9) | ||
// If one of the delimiters can both open and close emphasis, | ||
// then the sum of the lengths of the delimiter runs containing the opening and closing delimiters | ||
// must not be a multiple of 3 unless both lengths are multiples of 3 | ||
if (pendingOpener.type === '<>' || delimiter.type === '<>') { | ||
if ((pendingOpener.length + delimiter.length) % 3 === 0) { | ||
if (pendingOpener.length % 3 || delimiter.length % 3) { | ||
continue; | ||
} | ||
} | ||
} // it's a match! | ||
delimiter.type = '>'; | ||
let matchDelimiterLength = Math.min(delimiter.length, pendingOpener.length); // for each pair -> extract strong based on matchDelimiterInnerOffset | ||
while (matchDelimiterLength > 1) { | ||
matches.push(createInlineStyleMatch(text, 'strong', pendingOpener, delimiter)); | ||
matchDelimiterLength -= 2; | ||
} // if one left -> extract emphasis based on matchDelimiterInnerOffset | ||
if (matchDelimiterLength) { | ||
matches.push(createInlineStyleMatch(text, 'emphasis', pendingOpener, delimiter)); | ||
} // if opener is wider than closer | ||
if (pendingOpener.length) { | ||
// remove openers until current one (exclusive) | ||
pendingOpeners.splice(pendingOpenerIndex + 1); | ||
} else { | ||
// remove openers until current one (inclusive) | ||
pendingOpeners.splice(pendingOpenerIndex); // if closer is wider than opener -> look for more openers | ||
if (delimiter.length) { | ||
continue; | ||
} | ||
} | ||
break; | ||
} | ||
} | ||
if (delimiter.type !== '>') { | ||
pendingOpeners.push(delimiter); | ||
} | ||
}); | ||
@@ -55,18 +142,23 @@ matches.sort((a, b) => a.startIndex - b.startIndex); | ||
function getInlineMatchFromRegexpMatch(regexpMatch, inlineType) { | ||
const startIndex = regexpMatch.index; | ||
const innerText = regexpMatch[2]; | ||
function matchAll(regexp, text, onMatch) { | ||
let match; | ||
if (startIndex == null || !innerText) { | ||
return; | ||
while ((match = regexp.exec(text)) !== null) { | ||
onMatch(match); | ||
} | ||
} | ||
const decoratedText = regexpMatch[0]; | ||
const offset = decoratedText.indexOf(innerText); | ||
const endIndex = startIndex + decoratedText.length; | ||
function createInlineStyleMatch(text, type, opener, closer) { | ||
const innerTextStartIndex = opener.index + opener.length; | ||
const innerTextEndIndex = closer.index; | ||
const offset = type === 'strong' ? 2 : 1; // adjust delimiters | ||
opener.length -= offset; | ||
closer.length -= offset; | ||
closer.index += offset; | ||
return { | ||
type: inlineType, | ||
startIndex, | ||
endIndex, | ||
innerText, | ||
type, | ||
startIndex: innerTextStartIndex - offset, | ||
endIndex: innerTextEndIndex + offset, | ||
innerText: text.slice(innerTextStartIndex, innerTextEndIndex), | ||
offset | ||
@@ -76,9 +168,23 @@ }; | ||
function getCharInfo(char) { | ||
// detect spaces | ||
if (!char || /\s/.exec(char)) { | ||
return ' '; | ||
} // detect punctuation | ||
if (/[!"#$%&'()*+,.\/:;<=>?@[\\\]^_`{|}~-]/.exec(char)) { | ||
return '.'; | ||
} | ||
return; | ||
} | ||
function getSegmentsFromMatches(text, matches) { | ||
if (!matches.length) { | ||
return [text]; | ||
return [unescapeText(text)]; | ||
} | ||
const firstMatchStartIndex = matches[0].startIndex; | ||
const segments = firstMatchStartIndex > 0 ? [text.slice(0, firstMatchStartIndex)] : []; | ||
const segments = firstMatchStartIndex > 0 ? [unescapeText(text.slice(0, firstMatchStartIndex))] : []; | ||
@@ -88,14 +194,6 @@ while (matches.length) { | ||
const currentMatchTextStart = currentMatch.startIndex + currentMatch.offset; | ||
const innerMatches = []; | ||
const innerMatches = []; // find innerMatches | ||
for (let i = 0; i < matches.length;) { | ||
const otherMatch = matches[i]; // if not an inner match, continue to the next | ||
if (otherMatch.endIndex > currentMatch.endIndex) { | ||
i++; | ||
continue; | ||
} // remove it from matches | ||
matches.splice(i, 1); | ||
while (matches.length && matches[0].endIndex < currentMatch.endIndex) { | ||
const otherMatch = matches.shift(); | ||
otherMatch.startIndex -= currentMatchTextStart; | ||
@@ -124,3 +222,3 @@ otherMatch.endIndex -= currentMatchTextStart; | ||
if (textAfterLastMatch) { | ||
segments.push(textAfterLastMatch); | ||
segments.push(unescapeText(textAfterLastMatch)); | ||
} | ||
@@ -132,2 +230,7 @@ } | ||
function unescapeText(text) { | ||
// subset of escapable markdown chars which are used as markers in this lib | ||
return text.replace(/\\([*[\\\]_])/g, '$1'); | ||
} | ||
function Marksome({ | ||
@@ -134,0 +237,0 @@ text, |
{ | ||
"version": "0.3.0", | ||
"version": "1.0.0", | ||
"license": "MIT", | ||
@@ -45,7 +45,7 @@ "repository": "github:miguel-silva/react-marksome", | ||
"path": "dist/react-marksome.cjs.production.min.js", | ||
"limit": "1 KB" | ||
"limit": "1.5 KB" | ||
}, | ||
{ | ||
"path": "dist/react-marksome.esm.js", | ||
"limit": "1 KB" | ||
"limit": "1.5 KB" | ||
} | ||
@@ -76,4 +76,3 @@ ], | ||
"typescript": "^4.1.3" | ||
}, | ||
"dependencies": {} | ||
} | ||
} |
@@ -92,12 +92,33 @@ # react-marksome | ||
## Rationale | ||
## Supported Markdown | ||
The current subset of markdown that is supported is: | ||
- \*\*strong text\*\* | ||
- \*emphasized text\* | ||
- \[link description\]\[reference\] | ||
### Emphasis and strong emphasis | ||
By restricting ourselves to only support some markdown we're able to: | ||
_Emphasis_ (\*Emphasis\*) and **strong emphasis** (\*\*strong emphasis\*\*) parcing respects the [related commonmark spec section](https://spec.commonmark.org/0.30/#emphasis-and-strong-emphasis). | ||
### Link references | ||
Influenced by the [related commonmark spec section](https://spec.commonmark.org/0.30/#emphasis-and-strong-emphasis), link references can be defined in a couple of ways: | ||
- Full reference links: | ||
- input: \[react-marksome's Github page\]\[react-marksome github\] | ||
- output: [react-marksome's Github page][react-marksome github] | ||
- Shortcut reference links: | ||
- input: \[react-marksome github\] | ||
- output: [react-marksome github] | ||
There are certain quirks in marksome that are non-spec: | ||
1. it matches reference links regardless if the corresponding reference labels are defined as keys in the `references` prop or not | ||
2. reference labels are kept as is when looking for the corresponding key in `references` prop (ex: case-sensitive, no space-trimming, etc) | ||
3. nested squared brackets don't follow the same rules (ex: marksome supports unbalanced brackets) | ||
If reference links are not being matched as you desire, disable unintended matches by escaping the related opening (\\\[) or closing (\\\]) brackets. | ||
## Rationale | ||
By restricting ourselves to support only [some markdown](#supported-markdown) we're able to: | ||
- build a light package ([bundlephobia](https://bundlephobia.com/result?p=react-marksome)) | ||
@@ -120,38 +141,39 @@ - that provides a flexible, readable and condensed format for singleline pieces of text | ||
<p><strong>caniuse-lite db date: 15/02/2020</strong></p> | ||
<p><strong>caniuse-lite db date: 2nd Jan 2022</strong></p> | ||
<ul> | ||
<li>and_chr 87</li> | ||
<li>and_ff 83</li> | ||
<li>and_chr 97</li> | ||
<li>and_ff 95</li> | ||
<li>and_qq 10.4</li> | ||
<li>android 81</li> | ||
<li>chrome 87</li> | ||
<li>chrome 86</li> | ||
<li>chrome 85</li> | ||
<li>edge 87</li> | ||
<li>edge 86</li> | ||
<li>firefox 84</li> | ||
<li>firefox 83</li> | ||
<li>ios_saf 14.0-14.3</li> | ||
<li>android 97</li> | ||
<li>chrome 97</li> | ||
<li>chrome 96</li> | ||
<li>chrome 95</li> | ||
<li>chrome 94</li> | ||
<li>chrome 93</li> | ||
<li>chrome 92</li> | ||
<li>edge 97</li> | ||
<li>edge 96</li> | ||
<li>firefox 96</li> | ||
<li>firefox 95</li> | ||
<li>firefox 94</li> | ||
<li>ios_saf 15.2-15.3</li> | ||
<li>ios_saf 15.0-15.1</li> | ||
<li>ios_saf 14.5-14.8</li> | ||
<li>ios_saf 14.0-14.4</li> | ||
<li>ios_saf 13.4-13.7</li> | ||
<li>ios_saf 13.3</li> | ||
<li>ios_saf 13.2</li> | ||
<li>ios_saf 13.0-13.1</li> | ||
<li>ios_saf 12.2-12.4</li> | ||
<li>opera 72</li> | ||
<li>opera 71</li> | ||
<li>ios_saf 12.2-12.5</li> | ||
<li>op_mob 64</li> | ||
<li>opera 82</li> | ||
<li>opera 81</li> | ||
<li>safari 15.2-15.3</li> | ||
<li>safari 15.1</li> | ||
<li>safari 15</li> | ||
<li>safari 14.1</li> | ||
<li>safari 14</li> | ||
<li>safari 13.1</li> | ||
<li>safari 13</li> | ||
<li>samsung 13.0</li> | ||
<li>samsung 12.0</li> | ||
<li>samsung 16.0</li> | ||
<li>samsung 15.0</li> | ||
</ul> | ||
</details> | ||
## Alternatives | ||
If you're looking for wider markdown support: | ||
- [snarkdown](https://www.npmjs.com/package/snarkdown) for lightweight Markdown parser that returns plain HTML string | ||
- [markdown-to-jsx](https://www.npmjs.com/package/markdown-to-jsx) for a lot configurability and extensibility | ||
## Commands | ||
@@ -180,1 +202,3 @@ | ||
- [devuo](https://github.com/devuo) for providing some ideas and inspiration! | ||
[react-marksome github]: https://github.com/miguel-silva/react-marksome |
@@ -24,2 +24,9 @@ export type Segment = InlineStyleSegment | ReferenceLinkSegment | string; | ||
type InlineStyleDelimiter = { | ||
char: '*' | '_'; | ||
index: number; | ||
length: number; | ||
type: '<' | '>' | '<>'; | ||
}; | ||
type ReferenceLinkMatch = { | ||
@@ -34,34 +41,26 @@ type: 'reference-link'; | ||
const STRONG_TEXT_REGEXP = /([*_])\1\1?((?:\[.*?\][([].*?[)\]]|.)*?)\1?\1\1/g; | ||
// Capture [text][reference] or [reference] (taking into account escaped squared brackets) | ||
const REFERENCE_LINK_REGEXP = /(?<!\\)(?:\\\\)*(?:\[(.+?)(?<!\\)(?:\\\\)*\])?\[((?:(?<!\\)(?:\\\\)*\\[[]|[^[])+?)(?<!\\)(?:\\\\)*\]/g; | ||
const EMPHASIZED_TEXT_REGEXP = /([*_])((?:\[.*?\][([].*?[)\]]|.)*?)\1/g; | ||
// Capture sequence of '*' or '_' (non-escaped) | ||
const EMPH_SEQUENCE_REGEXP = /(?<!\\)(?:\\\\)*(\*+|_+)/g; | ||
const REFERENCE_LINK_TEXT_REGEXP = /\[([^\]]*)\] ?\[([^\]]*)\]/g; | ||
function matchAll( | ||
regexp: RegExp, | ||
text: string, | ||
onMatch: (match: RegExpExecArray) => void, | ||
) { | ||
let match: RegExpExecArray | null; | ||
while ((match = regexp.exec(text)) !== null) { | ||
onMatch(match); | ||
} | ||
} | ||
export function parseSegments(text: string): Segment[] { | ||
const matches: Match[] = []; | ||
matchAll(REFERENCE_LINK_TEXT_REGEXP, text, (referenceLinkRegExpMatch) => { | ||
const innerText = referenceLinkRegExpMatch[1]; | ||
const pendingOpenersByBlockIndex = new Map<number, InlineStyleDelimiter[]>([ | ||
[-1, []], | ||
]); | ||
const referenceLinkMatches: ReferenceLinkMatch[] = []; | ||
matchAll(REFERENCE_LINK_REGEXP, text, (referenceLinkRegExpMatch) => { | ||
const reference = referenceLinkRegExpMatch[2]; | ||
const startIndex = referenceLinkRegExpMatch.index; | ||
if (!innerText || !reference || startIndex == null) { | ||
return; | ||
} | ||
const innerText = referenceLinkRegExpMatch[1] || reference; | ||
const endIndex = startIndex + referenceLinkRegExpMatch[0].length; | ||
matches.push({ | ||
const match: ReferenceLinkMatch = { | ||
type: 'reference-link', | ||
@@ -73,25 +72,155 @@ innerText, | ||
offset: 1, | ||
}); | ||
}; | ||
pendingOpenersByBlockIndex.set(referenceLinkMatches.length, []); | ||
referenceLinkMatches.push(match); | ||
matches.push(match); | ||
}); | ||
matchAll(STRONG_TEXT_REGEXP, text, (strongRegExpMatch) => { | ||
const inlineMatch = getInlineMatchFromRegexpMatch( | ||
strongRegExpMatch, | ||
'strong', | ||
); | ||
let currentReferenceLinkIndex = 0; | ||
if (inlineMatch) { | ||
matches.push(inlineMatch); | ||
matchAll(EMPH_SEQUENCE_REGEXP, text, (emphCharRegExpMatch) => { | ||
const char = emphCharRegExpMatch[1][0] as '*' | '_'; | ||
const length = emphCharRegExpMatch[0].length; | ||
const index = emphCharRegExpMatch.index; | ||
const previousCharInfo = getCharInfo(text[index - 1]); | ||
const nextCharInfo = getCharInfo(text[index + length]); | ||
const leftFlanking = | ||
!nextCharInfo || (nextCharInfo === '.' && !!previousCharInfo); | ||
const rightFlanking = | ||
!previousCharInfo || (previousCharInfo === '.' && !!nextCharInfo); | ||
let canOpen = leftFlanking; | ||
let canClose = rightFlanking; | ||
if (char === '_') { | ||
canOpen = leftFlanking && (!rightFlanking || previousCharInfo === '.'); | ||
canClose = rightFlanking && (!leftFlanking || nextCharInfo === '.'); | ||
} | ||
}); | ||
matchAll(EMPHASIZED_TEXT_REGEXP, text, (emphasisRegExpMatch) => { | ||
const inlineMatch = getInlineMatchFromRegexpMatch( | ||
emphasisRegExpMatch, | ||
'emphasis', | ||
); | ||
if (!canOpen && !canClose) { | ||
return; | ||
} | ||
if (inlineMatch) { | ||
matches.push(inlineMatch); | ||
// identify current delimiter block index | ||
let blockIndex = -1; | ||
for ( | ||
; | ||
currentReferenceLinkIndex < referenceLinkMatches.length; | ||
currentReferenceLinkIndex++ | ||
) { | ||
const currentReferenceLinkMatch = | ||
referenceLinkMatches[currentReferenceLinkIndex]; | ||
// comes before the current block -> it's outside a block | ||
if (currentReferenceLinkMatch.startIndex > index) { | ||
break; | ||
} | ||
// comes after the current block -> check next block | ||
if (currentReferenceLinkMatch.endIndex <= index) { | ||
continue; | ||
} | ||
// it's inside the block but outside it's innerText -> ignore this emph char match | ||
if ( | ||
currentReferenceLinkMatch.startIndex + | ||
currentReferenceLinkMatch.offset + | ||
currentReferenceLinkMatch.innerText.length < | ||
index | ||
) { | ||
currentReferenceLinkIndex++; | ||
return; | ||
} | ||
blockIndex = currentReferenceLinkIndex; | ||
break; | ||
} | ||
const delimiter: InlineStyleDelimiter = { | ||
char, | ||
index, | ||
length, | ||
type: canOpen && canClose ? '<>' : canClose ? '>' : '<', | ||
}; | ||
const pendingOpeners = pendingOpenersByBlockIndex.get(blockIndex)!; | ||
// can close -> look for last compatible opener | ||
if (delimiter.type !== '<') { | ||
for ( | ||
let pendingOpenerIndex = pendingOpeners.length - 1; | ||
pendingOpenerIndex >= 0; | ||
pendingOpenerIndex-- | ||
) { | ||
const pendingOpener = pendingOpeners[pendingOpenerIndex]; | ||
// ensure that the pendingOpener is the same character | ||
if (pendingOpener.char !== delimiter.char) { | ||
continue; | ||
} | ||
// from spec (https://spec.commonmark.org/0.30/#emphasis-and-strong-emphasis rule 9) | ||
// If one of the delimiters can both open and close emphasis, | ||
// then the sum of the lengths of the delimiter runs containing the opening and closing delimiters | ||
// must not be a multiple of 3 unless both lengths are multiples of 3 | ||
if (pendingOpener.type === '<>' || delimiter.type === '<>') { | ||
if ((pendingOpener.length + delimiter.length) % 3 === 0) { | ||
if (pendingOpener.length % 3 || delimiter.length % 3) { | ||
continue; | ||
} | ||
} | ||
} | ||
// it's a match! | ||
delimiter.type = '>'; | ||
let matchDelimiterLength = Math.min( | ||
delimiter.length, | ||
pendingOpener.length, | ||
); | ||
// for each pair -> extract strong based on matchDelimiterInnerOffset | ||
while (matchDelimiterLength > 1) { | ||
matches.push( | ||
createInlineStyleMatch(text, 'strong', pendingOpener, delimiter), | ||
); | ||
matchDelimiterLength -= 2; | ||
} | ||
// if one left -> extract emphasis based on matchDelimiterInnerOffset | ||
if (matchDelimiterLength) { | ||
matches.push( | ||
createInlineStyleMatch(text, 'emphasis', pendingOpener, delimiter), | ||
); | ||
} | ||
// if opener is wider than closer | ||
if (pendingOpener.length) { | ||
// remove openers until current one (exclusive) | ||
pendingOpeners.splice(pendingOpenerIndex + 1); | ||
} else { | ||
// remove openers until current one (inclusive) | ||
pendingOpeners.splice(pendingOpenerIndex); | ||
// if closer is wider than opener -> look for more openers | ||
if (delimiter.length) { | ||
continue; | ||
} | ||
} | ||
break; | ||
} | ||
} | ||
if (delimiter.type !== '>') { | ||
pendingOpeners.push(delimiter); | ||
} | ||
}); | ||
@@ -104,25 +233,34 @@ | ||
function getInlineMatchFromRegexpMatch( | ||
regexpMatch: RegExpMatchArray, | ||
inlineType: 'strong' | 'emphasis', | ||
): InlineStyleMatch | undefined { | ||
const startIndex = regexpMatch.index; | ||
const innerText = regexpMatch[2]; | ||
if (startIndex == null || !innerText) { | ||
return; | ||
function matchAll( | ||
regexp: RegExp, | ||
text: string, | ||
onMatch: (match: RegExpExecArray) => void, | ||
) { | ||
let match: RegExpExecArray | null; | ||
while ((match = regexp.exec(text)) !== null) { | ||
onMatch(match); | ||
} | ||
} | ||
const decoratedText = regexpMatch[0]; | ||
function createInlineStyleMatch( | ||
text: string, | ||
type: 'strong' | 'emphasis', | ||
opener: InlineStyleDelimiter, | ||
closer: InlineStyleDelimiter, | ||
): InlineStyleMatch { | ||
const innerTextStartIndex = opener.index + opener.length; | ||
const innerTextEndIndex = closer.index; | ||
const offset = decoratedText.indexOf(innerText); | ||
const offset = type === 'strong' ? 2 : 1; | ||
const endIndex = startIndex + decoratedText.length; | ||
// adjust delimiters | ||
opener.length -= offset; | ||
closer.length -= offset; | ||
closer.index += offset; | ||
return { | ||
type: inlineType, | ||
startIndex, | ||
endIndex, | ||
innerText, | ||
type, | ||
startIndex: innerTextStartIndex - offset, | ||
endIndex: innerTextEndIndex + offset, | ||
innerText: text.slice(innerTextStartIndex, innerTextEndIndex), | ||
offset, | ||
@@ -132,5 +270,19 @@ }; | ||
function getCharInfo(char: string | undefined): ' ' | '.' | undefined { | ||
// detect spaces | ||
if (!char || /\s/.exec(char)) { | ||
return ' '; | ||
} | ||
// detect punctuation | ||
if (/[!"#$%&'()*+,.\/:;<=>?@[\\\]^_`{|}~-]/.exec(char)) { | ||
return '.'; | ||
} | ||
return; | ||
} | ||
function getSegmentsFromMatches(text: string, matches: Match[]): Segment[] { | ||
if (!matches.length) { | ||
return [text]; | ||
return [unescapeText(text)]; | ||
} | ||
@@ -141,3 +293,5 @@ | ||
const segments: Segment[] = | ||
firstMatchStartIndex > 0 ? [text.slice(0, firstMatchStartIndex)] : []; | ||
firstMatchStartIndex > 0 | ||
? [unescapeText(text.slice(0, firstMatchStartIndex))] | ||
: []; | ||
@@ -151,15 +305,6 @@ while (matches.length) { | ||
for (let i = 0; i < matches.length; ) { | ||
const otherMatch = matches[i]; | ||
// find innerMatches | ||
while (matches.length && matches[0].endIndex < currentMatch.endIndex) { | ||
const otherMatch = matches.shift()!; | ||
// if not an inner match, continue to the next | ||
if (otherMatch.endIndex > currentMatch.endIndex) { | ||
i++; | ||
continue; | ||
} | ||
// remove it from matches | ||
matches.splice(i, 1); | ||
otherMatch.startIndex -= currentMatchTextStart; | ||
@@ -194,3 +339,3 @@ otherMatch.endIndex -= currentMatchTextStart; | ||
if (textAfterLastMatch) { | ||
segments.push(textAfterLastMatch); | ||
segments.push(unescapeText(textAfterLastMatch)); | ||
} | ||
@@ -201,1 +346,6 @@ } | ||
} | ||
function unescapeText(text: string) { | ||
// subset of escapable markdown chars which are used as markers in this lib | ||
return text.replace(/\\([*[\\\]_])/g, '$1'); | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
102790
869
1
202