Skip to content

add numeric icon feature #762

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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 88 additions & 23 deletions src/steps/rewrite-icons.js
Original file line number Diff line number Diff line change
@@ -13,8 +13,6 @@
import { h } from 'hastscript';
import { CONTINUE, SKIP, visit } from 'unist-util-visit';

const REGEXP_ICON = /(?<!(?:https?|urn)[^\s]*):(#?[a-z_-]+[a-z\d]*):/gi;

/**
* Create a <span> icon element:
*
@@ -43,6 +41,7 @@ function createIcon(value) {
*/
export default function rewrite({ content }) {
const { hast } = content;

visit(hast, (node, idx, parent) => {
if (node.tagName === 'code') {
return SKIP;
@@ -52,31 +51,97 @@ export default function rewrite({ content }) {
}

const text = node.value;
let lastIdx = 0;
for (const match of text.matchAll(REGEXP_ICON)) {
const [matched, icon] = match;
const before = text.substring(lastIdx, match.index);
if (before) {
// textNode.parentNode.insertBefore(document.createTextNode(before), textNode);
parent.children.splice(idx, 0, { type: 'text', value: before });
idx += 1;
}
// textNode.parentNode.insertBefore(createIcon(document, icon), textNode);
parent.children.splice(idx, 0, createIcon(icon));
idx += 1;
lastIdx = match.index + matched.length;
const tokens = [];
let pos = 0;

// Find URN and timestamp patterns and mark their ranges
const skipRanges = [];

// URN patterns
const urnRegex = /urn:[^\s]*/g;
let urnMatch = urnRegex.exec(text);
while (urnMatch !== null) {
skipRanges.push([urnMatch.index, urnMatch.index + urnMatch[0].length]);
urnMatch = urnRegex.exec(text);
}

// Timestamp patterns (both real and placeholder)
const timeRegex = /(?:\d{4}|\b[A-Z]{4})-(?:\d{2}|[A-Z]{2})-(?:\d{2}|[A-Z]{2})T(?:\d{2}|[A-Z]{2}):(?:\d{2}|[A-Z]{2}):(?:\d{2}|[A-Z]{2})/g;
let timeMatch = timeRegex.exec(text);
while (timeMatch !== null) {
skipRanges.push([timeMatch.index, timeMatch.index + timeMatch[0].length]);
timeMatch = timeRegex.exec(text);
}

if (lastIdx && lastIdx <= text.length) {
// there is still some text left
const after = text.substring(lastIdx);
if (after) {
node.value = after;
while (pos < text.length) {
const colonPos = text.indexOf(':', pos);
if (colonPos === -1) {
tokens.push({ type: 'TEXT', value: text.slice(pos) });
break;
}

// Add text before the colon
if (colonPos > pos) {
tokens.push({ type: 'TEXT', value: text.slice(pos, colonPos) });
}

// Look for the closing colon
const nextColon = text.indexOf(':', colonPos + 1);
if (nextColon === -1) {
tokens.push({ type: 'TEXT', value: text.slice(colonPos) });
break;
}

const potentialIcon = text.slice(colonPos, nextColon + 1);
const beforeText = text.slice(Math.max(0, colonPos - 20), colonPos);

// Check if this colon is part of a skip range
const isInSkipRange = skipRanges.some((range) => colonPos >= range[0]
&& nextColon <= range[1]);

// Additional check for timestamp-like patterns
const isTimestampPattern = /[A-Z]{2}:[A-Z]{2}/.test(potentialIcon)
|| beforeText.match(/[A-Z]{2}$/)
|| text.slice(nextColon + 1).match(/^[A-Z]{2}/);

// Skip if this is part of a known pattern
const skipIfFound = [
/https?/, // URLs
/T\d{2}/, // ISO timestamps
/\d{4}-\d{2}/, // Dates
];

const shouldSkip = isInSkipRange
|| isTimestampPattern
|| skipIfFound.some((pattern) => pattern.test(beforeText))
|| /\d$/.test(beforeText) // number before first colon
|| /^\d/.test(text[nextColon + 1]); // number after second colon

if (shouldSkip) {
tokens.push({ type: 'TEXT', value: potentialIcon });
} else {
parent.children.splice(idx, 1);
idx -= 1;
const iconName = potentialIcon.slice(1, -1);
if (/^[#a-z0-9][-a-z0-9]*[a-z0-9]$/.test(iconName)) {
tokens.push({ type: 'ICON', value: iconName });
} else {
tokens.push({ type: 'TEXT', value: potentialIcon });
}
}

pos = nextColon + 1;
}

// Only process if we found any icons
if (!tokens.some((t) => t.type === 'ICON')) {
return CONTINUE;
}
return idx + 1;

// Convert tokens to nodes
const newNodes = tokens.map((token) => (
token.type === 'ICON' ? createIcon(token.value) : { type: 'text', value: token.value }
));

parent.children.splice(idx, 1, ...newNodes);
return idx + newNodes.length;
});
}
26 changes: 25 additions & 1 deletion test/fixtures/content/icons-ignored.html
Original file line number Diff line number Diff line change
@@ -5,5 +5,29 @@ <h1 id="icons">Icons</h1>
<pre><code>:rocket:</code></pre>
<p><a href="https://example.test/:urn:">https://example.test/:urn:</a></p>
<p>urn:aaid:sc:VA6C2:ac6066f3-fd1d-4e00-bed3-fa3aa6d981d8</p>
<p>:this is also ignored:</p>
<p>08:28:54</p>
<p>g) and 1:00-3:00 PM. H</p>
<p>6:00AM-6:00 PM MT</p>
<p>11:30am-12:30pm CT</p>
<p>c HEVC 4:2:2 10 bit</p>
<p>色情報が半分の4:2:2素材では、エッ</p>
<p>月1日木曜日14:00-19:00</p>
<p>Sec4:3-Sec4:6, Sec3:</p>
<p>ben Sie :1: ein, ge</p>
<p>168.0.52:4501:ssl</p>
<p>YYYY-MM-DDTHH:mm:ss.sssZ</p>
<p>2024-05-02T06:20:10.123Z</p>
<p>1:test: should be ignored</p>
<p>test:2: should also be ignored</p>
<p>x1:icon: should be ignored</p>
<p>:icon:2 should be ignored</p>
<p>00:00:00</p>
<p>This is just regular text with no icons</p><br><br>
<p>Empty text:</p>
<p>Non-text:<span></span></p>
<p>Mixed content: before :icon: between :button: after</p>
<p><span>:not-processed</span></p>
<p>Text with :1: number</p>
</div>
</main>
</main>
51 changes: 51 additions & 0 deletions test/fixtures/content/icons-ignored.md
Original file line number Diff line number Diff line change
@@ -9,3 +9,54 @@
[https://example.test/:urn:](https://example.test/:urn:)

urn:aaid:sc:VA6C2:ac6066f3-fd1d-4e00-bed3-fa3aa6d981d8

:this is also ignored:

08:28:54

g) and 1:00-3:00 PM. H

6:00AM-6:00 PM MT

11:30am-12:30pm CT

c HEVC 4:2:2 10 bit

色情報が半分の4:2:2素材では、エッ

月1日木曜日14:00-19:00

Sec4:3-Sec4:6, Sec3:

ben Sie :1: ein, ge

168.0.52:4501:ssl

YYYY-MM-DDTHH:mm:ss.sssZ

2024-05-02T06:20:10.123Z

1:test: should be ignored

test:2: should also be ignored

x1:icon: should be ignored

:icon:2 should be ignored

00:00:00

This is just regular text with no icons

<br>
<br>

Empty text:

Non-text: <span></span>

Mixed content: before :icon: between :button: after

<span>:not-processed</span>

Text with :1: number
24 changes: 20 additions & 4 deletions test/fixtures/content/icons.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
<main>
<div>
<h1 id="icons">Icons</h1>
<p>Hello <span class="icon icon-button"></span></p>
<p>Hello <span class="icon icon-red"></span> banner.</p>
<p>Hello <span class="icon icon-check"></span> mark.</p>
<p>Hello<span class="icon icon-button"></span></p>
<p>Hello <span class="icon icon-red"></span>banner.</p>
<p>Hello <span class="icon icon-check"></span>mark.</p>
<p>Team<span class="icon icon-rocket"></span>blasting off again.</p>
<p>This is a <span class="icon icon-a-10-check"></span>that should work</p>
<p>number check<span class="icon icon-1-check"></span></p>
<p>number check<span class="icon icon-two-2-check"></span></p>
<p>number check<span class="icon icon-three-3-check"></span></p>
<p>number check<span class="icon icon-four4check"></span></p>
<p>number check<span class="icon icon-5-check"></span></p>
<p>number check<span class="icon icon-six-6000-check"></span></p>
<p>number check<span class="icon icon-seven7000check"></span></p>
<p>number check<span class="icon icon-8000check"></span></p>
<p>number check<span class="icon icon-nine9000"></span></p>
<p>number check<span class="icon icon-ten-10000"></span></p>
<p>number check<span class="icon icon-eleven-11"></span></p>
<p>number check<span class="icon icon-twelve-v1"></span></p>
<p>icon named<span class="icon icon-icon"></span></p>
<p>for :foo:2 press the <span class="icon icon-button"></span> please.</p>
<p>Regular text <span class="icon icon-button"></span>more text <span class="icon icon-rocket"></span>final text</p>
</div>
</main>
</main>
33 changes: 33 additions & 0 deletions test/fixtures/content/icons.md
Original file line number Diff line number Diff line change
@@ -7,3 +7,36 @@ Hello :red: banner.
Hello :#check: mark.

Team:rocket:blasting off again.

This is a :a-10-check: that should work

number check :1-check:

number check :two-2-check:

number check :three-3-check:

number check :four4check:

number check :5-check:

number check :six-6000-check:

number check :seven7000check:

number check :8000check:

number check :nine9000:

number check :ten-10000:

number check :eleven-11:

number check :twelve-v1:

icon named :icon:

for :foo:2 press the :button: please.

Regular text :button: more text :rocket: final text


Unchanged files with check annotations Beta

}
const response = await render(url, '', expStatus, partition);
const actHtml = response.body;
console.log(actHtml);

Check warning on line 195 in test/rendering.test.js

GitHub Actions / Test

Unexpected console statement
if (expStatus === 200) {
const $actMain = new JSDOM(actHtml).window.document.querySelector(domSelector);
const $expMain = new JSDOM(expHtml).window.document.querySelector(domSelector);