Skip to content

Commit d03d38b

Browse files
committed
feat: Refine text cleaning logic by adjusting visible parameter spacing and removing a conflicting regex, adding new test cases, and updating a demo post.
1 parent 335d1af commit d03d38b

3 files changed

Lines changed: 168 additions & 9 deletions

File tree

_posts/2025-11-20-demo-update-dialog-alias.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ The `.Dialog` modifier is now fully supported in the demo! This feature allows y
1515

1616
In the demo, when you use:
1717
```musetag
18-
@@Sherlock.Dialog[Elementary, my dear Watson.]
18+
@@Sherlock.Dialog[: Elementary, my dear Watson.]
1919
```
2020

2121
It will now be rendered with a clear visual distinction:
22-
> **Sherlock:** Elementary, my dear Watson.
22+
> Sherlock: Elementary, my dear Watson.
2323
2424
And in the entity panel, you'll see a dedicated dialogue icon (💬) next to these occurrences, making it easy to scan through a character's spoken lines.
2525

@@ -33,7 +33,6 @@ We've also added support for the `.Alias` modifier. This is a game-changer for n
3333

3434
Once defined, any mention of "Holmes" in your text will be automatically detected as a reference to Sherlock. No need to spam `@@` tags everywhere!
3535

36-
Plus, we've improved the selection logic: clicking on an alias in the text (like "Holmes") will now automatically open the corresponding entity card (Sherlock), just as you'd expect.
3736

3837
## Try it out!
3938

assets/app.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,10 @@ document.addEventListener("DOMContentLoaded", () => {
4040
text = text.replace(/@@\.(?:[\w:!?]+(?:\([^)]*\))?)?(?!\[)/g, "");
4141

4242
// 3. Third pass: Remove other hidden annotations without visible parameters
43-
// - @@(...) without any visible parameters, like @@(Jules) or @@(1957-06-14)
44-
const hiddenAnnotationRegex = /@@\([^)]+\)(?!\[[^\]]*\])/gu;
45-
text = text.replace(hiddenAnnotationRegex, "");
43+
// - REMOVED as it was interfering with @@(Hidden).Modifier[Visible]
44+
// - Pass 4 should handle removal of hidden entities without visible params
45+
// const hiddenAnnotationRegex = /@@\([^)]+\)(?!\[[^\]]*\])/gu;
46+
// text = text.replace(hiddenAnnotationRegex, "");
4647

4748
// 4. Fourth pass: Process annotations that are meant to be visible.
4849
// This includes:
@@ -67,9 +68,10 @@ document.addEventListener("DOMContentLoaded", () => {
6768
// If it's a visible entity, output its name + visible params.
6869
if (visibleName) {
6970
let separator = "";
70-
// Check if modifiers contain .Dialog or .Voice
71-
if (/\.(?:Dialog|Voice)\b/.test(modifiersStr)) {
72-
separator = ": ";
71+
// Add space if visibleParamsOutput doesn't start with punctuation
72+
// Punctuation chars that shouldn't have preceding space: . , : ; ? ! )
73+
if (visibleParamsOutput.length > 0 && !/^[.,:;?!)]/.test(visibleParamsOutput)) {
74+
separator = " ";
7375
}
7476
return visibleName.replace(/_/g, " ") + separator + visibleParamsOutput;
7577
}

test_bugs.js

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
2+
// Mocking the necessary parts from app.js to test the parser
3+
4+
function getCleanText(rawText) {
5+
if (!rawText) return "";
6+
7+
let text = rawText;
8+
9+
// 1. First pass: Remove hidden structure annotations like @@.(# Title) specifically
10+
text = text.replace(/@@\.\([^)]*\)/g, "");
11+
12+
// 2. Second pass: Remove null entity patterns without visible parameters
13+
// - @@. (bare null entity)
14+
// - @@.modifier(hidden) patterns
15+
text = text.replace(/@@\.(?:[\w:!?]+(?:\([^)]*\))?)?(?!\[)/g, "");
16+
17+
// 3. Third pass: Remove other hidden annotations without visible parameters
18+
// - REMOVED as it was interfering with @@(Hidden).Modifier[Visible]
19+
// - Pass 4 should handle removal of hidden entities without visible params
20+
// const hiddenAnnotationRegex = /@@\([^)]+\)(?!\[[^\]]*\])/gu;
21+
// text = text.replace(hiddenAnnotationRegex, "");
22+
23+
// 4. Fourth pass: Process annotations that are meant to be visible.
24+
const visibleAnnotationRegex =
25+
/@@(?:([\p{L}\p{N}_]+)|\(([^)]+)\)|(\.))((?:\.[\w:!?]+(?:(?:\([^)]*\)|\[[^\]]*\]))?)*)/gu;
26+
27+
text = text.replace(
28+
visibleAnnotationRegex,
29+
(fullMatch, visibleName, hiddenContent, isNullDot, modifiersStr) => {
30+
// Extract content from visible parameters `[...]` only.
31+
const visibleParamRegex = /\[([^\]]*)\]/g;
32+
let visibleParamsOutput = "";
33+
let match;
34+
while ((match = visibleParamRegex.exec(modifiersStr)) !== null) {
35+
visibleParamsOutput += match[1];
36+
}
37+
38+
// If it's a visible entity, output its name + visible params.
39+
if (visibleName) {
40+
let separator = "";
41+
// Add space if visibleParamsOutput doesn't start with punctuation
42+
// Punctuation chars that shouldn't have preceding space: . , : ; ? ! )
43+
if (visibleParamsOutput.length > 0 && !/^[.,:;?!)]/.test(visibleParamsOutput)) {
44+
separator = " ";
45+
}
46+
return visibleName.replace(/_/g, " ") + separator + visibleParamsOutput;
47+
}
48+
49+
// If it's a hidden entity or null entity with visible parameters, output only the visible parameters.
50+
if ((hiddenContent || isNullDot) && visibleParamsOutput.length > 0) {
51+
return visibleParamsOutput;
52+
}
53+
54+
// This case should ideally not be reached, as fully hidden ones should be gone.
55+
// But as a fallback, return empty.
56+
return "";
57+
},
58+
);
59+
60+
// 5. Final cleanup: Remove any remaining modifier syntax that wasn't part of a match,
61+
// e.g. .modifier or .modifier(hidden) that was left over due to partial replacement
62+
text = text.replace(/\.[\w:!?]+(?:\([^)]*\))?/g, "");
63+
64+
return text;
65+
}
66+
67+
function parseMuseTag(rawText) {
68+
const cleanText = getCleanText(rawText);
69+
const entities = new Map();
70+
71+
// STEP 1: Parse all @@ annotations
72+
const annotationRegex =
73+
/(@{2,4})(?:([\p{L}\p{N}_]+)|\(([^)]+)\))((?:\.[\w:!?]+(?:(?:\(([^)]*)\)|\[[^\]]*\]))?)*)/gu;
74+
75+
let match;
76+
while ((match = annotationRegex.exec(rawText)) !== null) {
77+
const rawName = match[2] || match[3];
78+
const entityName = rawName.replace(/_/g, " ");
79+
const modifiersString = match[4] || "";
80+
81+
if (!entities.has(entityName)) {
82+
entities.set(entityName, {
83+
name: entityName,
84+
occurrences: []
85+
});
86+
}
87+
88+
const entityData = entities.get(entityName);
89+
const currentOccurrence = {
90+
position: match.index,
91+
localInfo: [],
92+
};
93+
94+
// Parse modifiers
95+
const modifierRegex = /\.([\w:!?]+)(?:(?:\(([^)]*)\)|\[([^\]]*)\]))?/g;
96+
let modMatch;
97+
while ((modMatch = modifierRegex.exec(modifiersString)) !== null) {
98+
const modName = modMatch[1];
99+
const modValue =
100+
modMatch[2] !== undefined
101+
? modMatch[2]
102+
: modMatch[3] !== undefined
103+
? modMatch[3]
104+
: null;
105+
106+
currentOccurrence.localInfo.push({ name: modName, value: modValue });
107+
}
108+
entityData.occurrences.push(currentOccurrence);
109+
}
110+
111+
return { cleanText, entities };
112+
}
113+
114+
// Test Cases
115+
console.log("--- Test Case 1: No automatic separator ---");
116+
const text1 = "@@Sherlock.Dialog[: Hello]";
117+
const result1 = parseMuseTag(text1);
118+
console.log(`Input: ${text1}`);
119+
console.log(`Output: ${result1.cleanText}`);
120+
console.log(`Expected: Sherlock: Hello`);
121+
if (result1.cleanText === "Sherlock: Hello") console.log("PASS"); else console.log("FAIL");
122+
123+
console.log("\n--- Test Case 2: Hidden entity with Dialog ---");
124+
const text2 = "@@(Sherlock).Dialog[Hello].";
125+
const result2 = parseMuseTag(text2);
126+
console.log(`Input: ${text2}`);
127+
console.log(`Output: ${result2.cleanText}`);
128+
console.log(`Expected: Hello.`);
129+
if (result2.cleanText === "Hello.") console.log("PASS"); else console.log("FAIL");
130+
131+
console.log("Entities found:");
132+
result2.entities.forEach((e, name) => {
133+
console.log(`Entity: ${name}`);
134+
console.log("Occurrences:", JSON.stringify(e.occurrences));
135+
});
136+
137+
console.log("\n--- Test Case 3: Hidden entity removal ---");
138+
const text3 = "This is @@(Hidden).";
139+
const result3 = parseMuseTag(text3);
140+
console.log(`Input: ${text3}`);
141+
console.log(`Output: ${result3.cleanText}`);
142+
console.log(`Expected: This is .`);
143+
if (result3.cleanText === "This is .") console.log("PASS"); else console.log("FAIL");
144+
console.log("\n--- Test Case 4: Spacing with text ---");
145+
const text4 = "@@Sherlock.Dialog[Elementary]";
146+
const result4 = parseMuseTag(text4);
147+
console.log(`Input: ${text4}`);
148+
console.log(`Output: ${result4.cleanText}`);
149+
console.log(`Expected: Sherlock Elementary`);
150+
if (result4.cleanText === "Sherlock Elementary") console.log("PASS"); else console.log("FAIL");
151+
152+
console.log("\n--- Test Case 5: Spacing with punctuation ---");
153+
const text5 = "@@Sherlock.Dialog[: Elementary]";
154+
const result5 = parseMuseTag(text5);
155+
console.log(`Input: ${text5}`);
156+
console.log(`Output: ${result5.cleanText}`);
157+
console.log(`Expected: Sherlock: Elementary`);
158+
if (result5.cleanText === "Sherlock: Elementary") console.log("PASS"); else console.log("FAIL");

0 commit comments

Comments
 (0)