diff --git a/src/elements/math.base.ts b/src/elements/math.base.ts
index 768886c36..1e1b2a301 100644
--- a/src/elements/math.base.ts
+++ b/src/elements/math.base.ts
@@ -7,6 +7,19 @@ export interface MathData {
isBlock: boolean;
}
+const DATA_FORMULA_BLOCK_TAGS = new Set([
+ 'address', 'article', 'aside', 'blockquote', 'div', 'figure',
+ 'footer', 'header', 'main', 'p', 'section'
+]);
+
+/**
+ * WeChat/mdnice renders the same formula in nested wrappers. Only the
+ * outermost data-formula element represents a formula in document flow;
+ * processing descendants as well would emit duplicate Markdown equations.
+ */
+export const isNestedDataFormula = (el: Element): boolean =>
+ el.hasAttribute('data-formula') && Boolean(el.parentElement?.closest('[data-formula]'));
+
// --- MathJax CHTML (mjx-*) → MathML reconstruction -------------------------
//
// When MathJax v3 renders with the CHTML output and the page ships no
@@ -304,6 +317,10 @@ export const getBasicLatexFromElement = (el: Element): string | null => {
if (dataMath) {
return dataMath;
}
+ const dataFormula = el.getAttribute('data-formula');
+ if (dataFormula) {
+ return dataFormula;
+ }
const parentDataEntry = el.parentElement?.classList.contains('hurmet-tex')
? el.parentElement.getAttribute('data-entry')
: null;
@@ -375,6 +392,22 @@ export const isBlockDisplay = (el: Element): boolean => {
return true;
}
+ // WeChat/mdnice uses semantic block wrappers (commonly section/div) for
+ // display equations and span wrappers for inline equations. Explicit
+ // display hints take precedence over the wrapper tag.
+ if (el.hasAttribute('data-formula')) {
+ const dataDisplayAttr = el.getAttribute('data-display');
+ if (displayAttr === 'inline' || displayAttr === 'false' || dataDisplayAttr === 'inline' || dataDisplayAttr === 'false') {
+ return false;
+ }
+ if (displayAttr === 'true' || dataDisplayAttr === 'block' || dataDisplayAttr === 'true') {
+ return true;
+ }
+ if (DATA_FORMULA_BLOCK_TAGS.has(el.tagName.toLowerCase())) {
+ return true;
+ }
+ }
+
// Check common class names
const classNames = getClassName(el).toLowerCase();
if (classNames.includes('display') || classNames.includes('block')) {
@@ -426,7 +459,7 @@ export const isBlockDisplay = (el: Element): boolean => {
// Cheap presence check before running the full mathSelectors scan.
// Must remain a subset of mathSelectors — every selector here should also appear there.
-export const mathFastCheck = 'math, mjx-container, .MathJax, .katex, img.latex, [data-math], [data-latex], script[type^="math/"]';
+export const mathFastCheck = 'math, mjx-container, .MathJax, .katex, img.latex, [data-math], [data-latex], [data-formula], script[type^="math/"]';
// Shared selector for math elements
export const mathSelectors = [
@@ -462,6 +495,7 @@ export const mathSelectors = [
'math',
'[data-math]',
'[data-latex]',
+ '[data-formula]',
'[data-tex]',
'script[type^="math/"]',
'annotation[encoding="application/x-tex"]'
diff --git a/src/elements/math.core.ts b/src/elements/math.core.ts
index 9299d64af..7790a2434 100644
--- a/src/elements/math.core.ts
+++ b/src/elements/math.core.ts
@@ -3,6 +3,7 @@ import {
getMathMLFromElement,
getBasicLatexFromElement as getLatexFromElement,
isBlockDisplay,
+ isNestedDataFormula,
mathSelectors,
mathFastCheck
} from './math.base';
@@ -43,6 +44,7 @@ export const mathRules = [
fastCheck: mathFastCheck,
transform: (el: Element, doc: Document): Element => {
if (!hasHTMLElementProps(el)) return el;
+ if (isNestedDataFormula(el)) return el;
const mathData = getMathMLFromElement(el);
const latex = getLatexFromElement(el);
@@ -62,4 +64,4 @@ export const mathRules = [
return cleanMathEl;
}
}
-];
\ No newline at end of file
+];
diff --git a/src/elements/math.full.ts b/src/elements/math.full.ts
index 8b358ff3f..ad35a40fd 100644
--- a/src/elements/math.full.ts
+++ b/src/elements/math.full.ts
@@ -4,6 +4,7 @@ import {
getMathMLFromElement,
getBasicLatexFromElement,
isBlockDisplay,
+ isNestedDataFormula,
mathSelectors,
mathFastCheck
} from './math.base';
@@ -79,6 +80,7 @@ export const mathRules = [
if (!('classList' in el) || !('getAttribute' in el) || !('querySelector' in el)) {
return el;
}
+ if (isNestedDataFormula(el)) return el;
const mathData = getMathMLFromElement(el);
const latex = getLatexFromElement(el);
@@ -98,4 +100,4 @@ export const mathRules = [
return cleanMathEl;
}
}
-];
\ No newline at end of file
+];
diff --git a/tests/expected/math--wechat-data-formula.md b/tests/expected/math--wechat-data-formula.md
new file mode 100644
index 000000000..37e8afe04
--- /dev/null
+++ b/tests/expected/math--wechat-data-formula.md
@@ -0,0 +1,106 @@
+```json
+{
+ "title": "WeChat data-formula",
+ "author": "",
+ "site": "",
+ "published": ""
+}
+```
+
+Inline formula $x_1$ should appear once.
+
+Equation 1.
+
+$$
+E_1 = x_1 \tag{1}
+$$
+
+Equation 2.
+
+$$
+E_2 = x_2 \tag{2}
+$$
+
+Equation 3.
+
+$$
+E_3 = x_3 \tag{3}
+$$
+
+Equation 4.
+
+$$
+E_4 = x_4 \tag{4}
+$$
+
+Equation 5.
+
+$$
+E_5 = x_5 \tag{5}
+$$
+
+Equation 6.
+
+$$
+E_6 = x_6 \tag{6}
+$$
+
+Equation 7.
+
+$$
+E_7 = x_7 \tag{7}
+$$
+
+Equation 8.
+
+$$
+E_8 = x_8 \tag{8}
+$$
+
+Equation 9.
+
+$$
+E_9 = x_9 \tag{9}
+$$
+
+Equation 10.
+
+$$
+E_{10} = x_{10} \tag{10}
+$$
+
+Equation 11.
+
+$$
+E_{11} = x_{11} \tag{11}
+$$
+
+Equation 12.
+
+$$
+E_{12} = x_{12} \tag{12}
+$$
+
+Equation 13.
+
+$$
+E_{13} = x_{13} \tag{13}
+$$
+
+Equation 14.
+
+$$
+E_{14} = x_{14} \tag{14}
+$$
+
+Equation 15.
+
+$$
+E_{15} = x_{15} \tag{15}
+$$
+
+Equation 16.
+
+$$
+E_{16} = x_{16} \tag{16}
+$$
diff --git a/tests/fixtures/math--wechat-data-formula.html b/tests/fixtures/math--wechat-data-formula.html
new file mode 100644
index 000000000..8e4ee33e8
--- /dev/null
+++ b/tests/fixtures/math--wechat-data-formula.html
@@ -0,0 +1,84 @@
+
+
+
+
+
+ WeChat data-formula
+
+
+
+ WeChat data-formula
+
+ Inline formula
+
+
+
+ should appear once.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+