diff --git a/README.md b/README.md
index ee673a9..8dcf343 100644
--- a/README.md
+++ b/README.md
@@ -1,33 +1,56 @@
pastefromhtml
=============
-
-pastefromhtml is a Zim plugin that lets you paste lists, links, text, and now images (thanks to an anonymous commit) from html clipboard data.
+pastefromhtml is a Zim plugin that lets you paste lists, links, text, images, and LaTeX/MathML equations from HTML clipboard data.
#### Installation
+You can install pastefromhtml by creating a folder with files `htmlcdparser.py` and `__init__.py` in your zim/plugins directory.
-You can install pastefromhtml plugin in Zim, by creating a folder with files `htmlcdparser.py` and `__init__.py` in your zim/plugins directory.
-
-For installation in Linux, you can place a folder (with files `htmlcdparser.py` and `__init__.py` inside) in `~/.local/share/zim/plugins` directory.
-
-pastefromhtml plugin can also be installed by cloning the github repository in zim/plugins directory:
+For installation in Linux, place the folder in `~/.local/share/zim/plugins`.
+You can also install by cloning the repository:
```sh
cd ~/.local/share/zim/plugins
git clone https://github.com/cetinkaya/pastefromhtml.git
```
-To enable the plugin, click on the menu entry `Edit / Preferences`, then go to `Plugins` tab.
+To enable the plugin, click on `Edit / Preferences`, then go to the `Plugins` tab.
+#### Dependencies
-#### Use
+For basic use (text, links, images) no additional dependencies are needed.
+
+For **LaTeX/KaTeX equation rendering** (e.g. when pasting from Claude.ai) the following must be installed:
+- `latex` and `dvipng` (from TeX Live)
+
+For **MathML equation rendering** (e.g. when pasting from GitHub Copilot) additionally:
+- `python-pypandoc` and `pandoc`
+
+On Arch/Manjaro:
+```sh
+sudo pacman -S texlive dvipng python-pypandoc
+```
-pastefromhtml adds menu entries (`Tools / Paste from HTML`, `Edit / Paste from HTML`) and a right-click context-menu entry, as well as a keyboard shortcut (`ctrl + shift + v`) in Zim.
+On Debian/Ubuntu:
+```sh
+sudo apt install texlive dvipng pandoc python3-pypandoc
+```
+
+If the dependencies are not installed, equations are silently skipped and the rest of the content is pasted normally.
+
+#### Use
+pastefromhtml adds menu entries (`Tools / Paste from HTML`, `Edit / Paste from HTML`) and a right-click context-menu entry, as well as a keyboard shortcut (`ctrl + shift + v`) in Zim.
-After you copy lists/links/text/images in your browser, paste them into Zim by clicking on the menu entry or by using the shortcut `ctrl + shift + v`.
+After you copy content in your browser, paste it into Zim by clicking on the menu entry or by using the shortcut `ctrl + shift + v`.
+Equations are automatically rendered to PNG and embedded as images in the page's attachments folder.
-#### Quirks
+#### Configuration
+To access the configuration options, click on `Edit / Preferences`, navigate to the `Plugins` tab, select `Paste from HTML`, and click the `Configure` button.
-##### Images inside anchor tags
+Available options:
-When pasting image tags `
` that are placed inside anchor tags ``, the default behavior of pastefromhtml is to only paste the URL that the anchor tag refers to (unless it is the URL of the image). There is an option that can be configured so as to paste the images instead of URLs of the anchor tags. To access this option, you can click on Edit/Preferences in the menu bar, navigate to the Plugins tab, and select Paste from HTML. After that, click on the Configure button located on the right-hand side.
+- **Images inside anchor tags** – when pasting `
` tags inside `` tags, paste the image instead of the link URL
+- **Reload page after pasting** – reload the page after pasting to apply formatting
+- **LaTeX equation font size** – font size in pt for rendered equations (default: 14)
+- **LaTeX equation image DPI** – resolution of rendered equation images (default: 200)
+- **LaTeX dark mode** – use white font color for equations (for dark Zim themes)
diff --git a/__init__.py b/__init__.py
index 70ca866..c4e981b 100755
--- a/__init__.py
+++ b/__init__.py
@@ -23,7 +23,14 @@
class PasteFromHTMLPlugin(PluginClass):
plugin_info = {
"name": _("Paste from HTML"),
- "description": _("This plugin lets you paste HTML clipboard data."),
+ "description": _(
+ "This plugin lets you paste HTML clipboard data.\n\n"
+ "For LaTeX/KaTeX equation rendering the following must be installed:\n"
+ " - latex (texlive)\n"
+ " - dvipng\n"
+ " - python-pypandoc (for MathML from Copilot etc.)\n\n"
+ "On Arch/Manjaro: sudo pacman -S texlive dvipng python-pypandoc"
+ ),
"author": "Ahmet Cetinkaya",
"help": "Plugins:Paste from HTML"
}
@@ -32,6 +39,9 @@ class PasteFromHTMLPlugin(PluginClass):
# key, type, label, default
('image_inside_a', 'bool', _('For image tags
inside anchor tags , paste images instead of links'), False),
('reload_page', 'bool', _('Reload page after pasting'), False),
+ ('latex_font_size', 'int', _('LaTeX equation font size (pt)'), 14, (6, 24)),
+ ('latex_dpi', 'choice', _('LaTeX equation image DPI'), '200', ('96', '120', '150', '200', '300', '400', '600')),
+ ('latex_dark_mode', 'bool', _('LaTeX equations: use white font (for dark themes)'), False),
)
@@ -95,7 +105,12 @@ def pastefh(self):
if target in ["text/html", "TEXT/HTML", "HTML Format"]:
data = get_fragment(data)
cursor = self.window.pageview.get_cursor_pos()
- h = HTMLCDParser(self.preferences['image_inside_a'])
+ h = HTMLCDParser(
+ self.preferences['image_inside_a'],
+ latex_font_size=self.preferences['latex_font_size'],
+ latex_dpi=int(self.preferences['latex_dpi']),
+ latex_dark_mode=self.preferences['latex_dark_mode'],
+ )
buffer.insert_at_cursor(h.to_zim(data, folder))
self.window.pageview.set_page(self.window.pageview.page)
self.window.pageview.set_cursor_pos(cursor)
diff --git a/htmlcdparser.py b/htmlcdparser.py
index a212fa3..79ec90e 100755
--- a/htmlcdparser.py
+++ b/htmlcdparser.py
@@ -17,6 +17,80 @@
import re
import urllib.request, urllib.error, urllib.parse
import os
+import unicodedata
+import glob
+import subprocess
+import tempfile
+import shutil
+
+try:
+ import pypandoc
+ HAS_PYPANDOC = True
+except ImportError:
+ HAS_PYPANDOC = False
+
+def _fix_pandoc_latex(latex_src):
+ """Fix common issues in pandoc's MathML→LaTeX output."""
+ # \overset{˙}{x} → \dot{x} (dot accent)
+ latex_src = re.sub(r'\\overset\{˙\}\{([^}]+)\}', r'\\dot{\1}', latex_src)
+ # \overset{¨}{x} → \ddot{x} (double dot accent)
+ latex_src = re.sub(r'\\overset\{¨\}\{([^}]+)\}', r'\\ddot{\1}', latex_src)
+ # \overset{`}{x} → \grave{x}
+ latex_src = re.sub(r'\\overset\{`\}\{([^}]+)\}', r'\\grave{\1}', latex_src)
+ # \overset{´}{x} → \acute{x}
+ latex_src = re.sub(r'\\overset\{´\}\{([^}]+)\}', r'\\acute{\1}', latex_src)
+ # \overset{^}{x} → \hat{x}
+ latex_src = re.sub(r'\\overset\{\^\}\{([^}]+)\}', r'\\hat{\1}', latex_src)
+ # \overset{~}{x} → \tilde{x}
+ latex_src = re.sub(r'\\overset\{~\}\{([^}]+)\}', r'\\tilde{\1}', latex_src)
+ # \parallel → \| (standard LaTeX norm)
+ latex_src = latex_src.replace(r'\parallel', r'\|')
+ return latex_src
+
+
+def _mathml_to_latex(mathml_str):
+ """Convert a MathML string to LaTeX via pandoc. Returns None on failure."""
+ if not HAS_PYPANDOC:
+ return None
+ try:
+ html = '{}'.format(mathml_str)
+ result = pypandoc.convert_text(html, 'latex', format='html+tex_math_dollars')
+ result = result.strip()
+ for wrapper in (r'\[', r'\]', r'\(', r'\)', '$$', '$'):
+ result = result.replace(wrapper, '')
+ result = _fix_pandoc_latex(result.strip())
+ return result
+ except Exception:
+ return None
+
+# Set to True to open a terminal with a debug log after each paste operation
+DEBUG = False
+
+def _dbg(msg, loglines=None):
+ if not DEBUG:
+ return
+ if loglines is not None:
+ loglines.append(str(msg))
+
+def _show_debug_terminal(lines):
+ """Write debug log to a tmp file and open it in a terminal."""
+ if not DEBUG:
+ return
+ log_path = '/tmp/pastefromhtml_debug.log'
+ with open(log_path, 'w', encoding='utf-8') as f:
+ f.write('\n'.join(lines))
+ # Try common terminal emulators in order
+ for term in [
+ ['terminator', '--new-tab', '-e', 'bash -c "cat {}; echo; echo \'--- press enter to close ---\'; read"'.format(log_path)],
+ ['gnome-terminal', '--', 'bash', '-c', 'cat {}; echo; echo "--- press enter to close ---"; read'.format(log_path)],
+ ['xterm', '-e', 'bash', '-c', 'cat {}; echo; echo "--- press enter to close ---"; read'.format(log_path)],
+ ['konsole', '-e', 'bash', '-c', 'cat {}; echo; echo "--- press enter to close ---"; read'.format(log_path)],
+ ]:
+ try:
+ subprocess.Popen(term)
+ return
+ except FileNotFoundError:
+ continue
def assoc(key, pairs):
value = None
@@ -34,11 +108,163 @@ def get_url(url, name):
f.write(urllib.request.urlopen(req).read())
f.close()
-# HTML Clipboard Data Parser
+
+# --- Inline LaTeX renderer (extracted from zim equationeditor plugin) --------
+
+LATEX_TEMPLATE = r"""\documentclass[%(font_size)spt]{article}
+\usepackage{amsmath}
+\usepackage{amssymb}
+\usepackage{amsthm}
+%(color_preamble)s
+\pagestyle{empty}
+\begin{document}
+%(color_begin)s
+\begin{math}
+%(equation)s
+\end{math}
+%(color_end)s
+\end{document}
+"""
+
+def _latex_to_png(latex_src, output_png_path, font_size=14, dark_mode=False, dpi=150, log=None):
+ """
+ Renders a LaTeX math expression to a PNG file.
+ Returns True on success, False on failure.
+ latex_src: raw LaTeX string (without \\begin{math}...\\end{math} wrapper)
+ output_png_path: absolute path where the PNG should be written
+ """
+ if log is None:
+ log = []
+ _dbg('=== _latex_to_png called ===', log)
+ _dbg('latex_src: ' + repr(latex_src), log)
+ _dbg('output_png_path: ' + output_png_path, log)
+
+ # Filter empty lines (not allowed in latex equation blocks)
+ lines = [l for l in latex_src.splitlines(True) if l and not l.isspace()]
+ equation = ''.join(lines).strip()
+ _dbg('equation after filter: ' + repr(equation), log)
+ if not equation:
+ _dbg('ABORT: equation is empty', log)
+ return False
+
+ if dark_mode:
+ color_preamble = r'\usepackage{color}'
+ color_begin = r'\color{white}'
+ color_end = ''
+ else:
+ color_preamble = ''
+ color_begin = ''
+ color_end = ''
+
+ tex_source = LATEX_TEMPLATE % {
+ 'font_size': font_size,
+ 'equation': equation,
+ 'color_preamble': color_preamble,
+ 'color_begin': color_begin,
+ 'color_end': color_end,
+ }
+
+ tmpdir = tempfile.mkdtemp(prefix='zim_eq_')
+ _dbg('tmpdir: ' + tmpdir, log)
+ try:
+ tex_path = os.path.join(tmpdir, 'equation.tex')
+ dvi_path = os.path.join(tmpdir, 'equation.dvi')
+ png_path = os.path.join(tmpdir, 'equation.png')
+
+ with open(tex_path, 'w', encoding='utf-8') as f:
+ f.write(tex_source)
+ _dbg('tex_source written to: ' + tex_path, log)
+
+ # Run latex
+ result = subprocess.run(
+ ['latex', '-no-shell-escape', '-halt-on-error', 'equation.tex'],
+ cwd=tmpdir,
+ capture_output=True
+ )
+ _dbg('latex returncode: ' + str(result.returncode), log)
+ _dbg('latex stdout: ' + result.stdout.decode('utf-8', errors='replace'), log)
+ _dbg('latex stderr: ' + result.stderr.decode('utf-8', errors='replace'), log)
+ if result.returncode != 0:
+ _dbg('ABORT: latex failed', log)
+ return False
+
+ # Run dvipng
+ result = subprocess.run(
+ ['dvipng', '-q', '-bg', 'Transparent', '-T', 'tight',
+ '-D', str(dpi), '-o', png_path, dvi_path],
+ cwd=tmpdir,
+ capture_output=True
+ )
+ _dbg('dvipng returncode: ' + str(result.returncode), log)
+ _dbg('dvipng stdout: ' + result.stdout.decode('utf-8', errors='replace'), log)
+ _dbg('dvipng stderr: ' + result.stderr.decode('utf-8', errors='replace'), log)
+ if result.returncode != 0:
+ _dbg('ABORT: dvipng failed', log)
+ return False
+
+ _dbg('png_path exists: ' + str(os.path.exists(png_path)), log)
+ shutil.copy2(png_path, output_png_path)
+ _dbg('SUCCESS: copied to ' + output_png_path, log)
+ return True
+ except Exception as e:
+ _dbg('EXCEPTION: ' + str(e), log)
+ return False
+ finally:
+ if not DEBUG:
+ shutil.rmtree(tmpdir, ignore_errors=True)
+ else:
+ _dbg('tmpdir kept for inspection (DEBUG=True): ' + tmpdir, log)
+
+
+def _equation_png_name(latex_src):
+ """Generate a readable filename from the LaTeX source."""
+ name = latex_src.strip()
+ # Replace characters that are problematic in filenames
+ replacements = {
+ '\\': '_',
+ '/': '_',
+ '{': '_',
+ '}': '_',
+ '^': '_',
+ '*': '_',
+ '?': '_',
+ '"': '_',
+ '<': '_',
+ '>': '_',
+ '|': '_',
+ ':': '_',
+ ' ': '_',
+ '\n': '_',
+ '\t': '_',
+ '=': 'eq',
+ '+': 'plus',
+ '-': 'minus',
+ }
+ for char, replacement in replacements.items():
+ name = name.replace(char, replacement)
+ # Collapse multiple underscores
+ name = re.sub(r'_+', '_', name).strip('_')
+ # Limit length to avoid filesystem issues
+ if len(name) > 80:
+ name = name[:80]
+ return 'eq_%s.png' % name
+
+
+# --- HTML Clipboard Data Parser ----------------------------------------------
+
class HTMLCDParser(HTMLParser):
- def __init__(self, image_inside_a):
- HTMLParser.__init__(self) # super().__init__() for Pyhon 3
+ def __init__(self, image_inside_a, latex_font_size=14, latex_dark_mode=False, latex_dpi=150):
+ HTMLParser.__init__(self)
self.image_inside_a = image_inside_a
+
+ # LaTeX rendering settings
+ self.latex_font_size = latex_font_size
+ self.latex_dark_mode = latex_dark_mode
+ self.latex_dpi = latex_dpi
+
+ # Debug log (populated only when DEBUG=True)
+ self._log = []
+
self.zim_str = ""
self.beg = {"h1": "====== ",
"h2": "===== ",
@@ -71,7 +297,7 @@ def __init__(self, image_inside_a):
"tr": "",
"th": "|",
"td": "|",
- "hr": "-----\n",
+ # hr intentionally omitted – handled in handle_startendtag only
"sup": "^{",
"sub": "_{",
"span": "",
@@ -122,12 +348,12 @@ def __init__(self, image_inside_a):
self.inside_pre = False
self.pre_data = ""
self.inside_blockquote = False
- self.inside_tag = "" #Indicate label on which we are
- self.start_tag = "" #Initial tag in case we have to delete it
+ self.inside_tag = ""
+ self.start_tag = ""
self.del_tag = ""
- self.tag_attrib = "" #Tag Attribute Value
+ self.tag_attrib = ""
self.folder = None
- self.a_href = "" #Link of a tag
+ self.a_href = ""
self.inside_li = False
self.list_level = -1
self.inside_iframe = False
@@ -135,29 +361,140 @@ def __init__(self, image_inside_a):
self.inside_dl = False
self.inside_table = False
+ # KaTeX / LaTeX state
+ self.inside_katex = False # inside any .katex element
+ self.inside_annotation = False # inside
+ self.katex_depth = 0 # nesting depth within .katex
+ self.annotation_src = "" # accumulated raw LaTeX source
+ self.katex_rendered = False # did we already emit a PNG for current expression?
+ self.inside_katex_html = False # inside aria-hidden katex-html (skip rendering noise)
+
+ # MathML state (Copilot and other sources)
+ self.inside_mathml = False # inside a – finalize
+ self.mathml_raw += ''
+ _dbg('MathML END raw={}'.format(repr(self.mathml_raw[:80])), self._log)
+ latex_src = _mathml_to_latex(self.mathml_raw)
+ _dbg('MathML→LaTeX: {}'.format(repr(latex_src)), self._log)
+ if latex_src:
+ zim_img = self._render_equation(latex_src)
+ _dbg('zim_img result: ' + str(zim_img), self._log)
+ if zim_img:
+ if not self.zim_str.endswith((" ", "\n", "\t")):
+ self.zim_str += " "
+ self.zim_str += zim_img
+ self.inside_mathml = False
+ self.mathml_depth = 0
+ self.mathml_raw = ""
+ else:
+ self.mathml_raw += '{}>'.format(tag)
+ return
+
+ # ---- KaTeX handling ----
+ if self.inside_katex:
+ if tag == 'annotation' and self.inside_annotation:
+ _dbg('annotation END src={}'.format(repr(self.annotation_src)), self._log)
+ if self.annotation_src.strip() and not self.katex_rendered:
+ zim_img = self._render_equation(self.annotation_src)
+ _dbg('zim_img result: ' + str(zim_img), self._log)
+ if zim_img:
+ if not self.zim_str.endswith((" ", "\n", "\t")):
+ self.zim_str += " "
+ self.zim_str += zim_img
+ self.katex_rendered = True
+ self.inside_annotation = False
+ self.annotation_src = ""
+ self.katex_depth -= 1
+ _dbg('KaTeX END tag={} depth now={}'.format(tag, self.katex_depth), self._log)
+ if self.katex_depth <= 0:
+ self.inside_katex = False
+ self.inside_katex_html = False
+ self.katex_depth = 0
+ return
+
+ # ---- Normal processing ----
+ if self.inside_tag and tag != self.inside_tag and not (self.inside_tag == "th" or self.inside_tag == "td" or self.inside_tag == "dt" or self.inside_tag == "dd") and not (tag == "a" and (self.inside_tag == "b" or self.inside_tag == "strong" or self.inside_tag == "i" or self.inside_tag == "em" or self.inside_tag == "u" or self.inside_tag == "ins" or self.inside_tag == "mark" or self.inside_tag == "strike" or self.inside_tag == "del") and self.del_tag):
return
if tag == "blockquote":
self.inside_blockquote = False
- #end of nestable tag
if self.inside_tag == tag:
- self.inside_tag = "";
- #Init href of tag a
+ self.inside_tag = ""
if tag == "a":
self.a_href = ""
- #If you tag this within another non-nestable it is deleted
if self.del_tag == tag:
self.start_tag = ""
self.del_tag = ""
return
if (tag == "pre" or tag == "code"):
- #If tag empty del start tag
if not self.pre_data:
if self.zim_str.endswith("''"):
self.zim_str = self.zim_str[:len(self.zim_str) - 2]
@@ -292,12 +661,10 @@ def handle_endtag(self, tag):
self.inside_pre = False
return
if self.pre_data.count('\n') > 0:
- #Initial tag
if self.zim_str.endswith("''"):
self.zim_str = self.zim_str[:len(self.zim_str) - 2]
self.zim_str += "\n'''\n"
self.zim_str += self.pre_data
- #Final Tag
if self.pre_data.count('\n') > 0:
self.zim_str += "\n'''\n"
else:
@@ -305,7 +672,6 @@ def handle_endtag(self, tag):
self.pre_data = ""
self.inside_pre = False
return
- #Remove enter before tr, td, th
if tag == "tr" or tag == "td" or tag == "th":
if self.zim_str.endswith("\n"):
self.zim_str = self.zim_str[:len(self.zim_str) - len("\n")]
@@ -340,71 +706,84 @@ def handle_endtag(self, tag):
if tag in list(self.end.keys()):
self.start_tag = ""
if tag == "li":
- #If li empty del
if self.list_type == "ul" and self.zim_str.endswith("* "):
self.zim_str = self.zim_str[:len(self.zim_str) - 2]
elif self.list_type == "ol" and self.zim_str.endswith(str(self.item_no) + ". "):
self.zim_str = self.zim_str[:len(self.zim_str) - len(str(self.item_no) + ". ")]
self.item_no -= 1
- #Add enter if not exists in li tag
elif not self.zim_str.endswith("\n"):
self.zim_str += "\n"
else:
- #If we are not at a level higher than the first level of a list and not two last tag finish in \n
if not ((tag == "ol" or tag == "ul") and self.list_level >= 0):
- #If tag empty then delete
if tag in list(self.beg.keys()) and self.beg[tag] and self.zim_str.endswith(self.beg[tag]):
self.zim_str = self.zim_str[:len(self.zim_str) - len(self.beg[tag])]
else:
- #If not duplicate \n
if not ((tag == "ol" or tag == "ul") and self.zim_str.endswith("\n")):
self.zim_str += self.end[tag]
- #If tag li end inside_li false
if tag == "li" or tag == "ul" or tag == "ol":
- self.inside_li = False
+ self.inside_li = False
def handle_data(self, data):
- if self.inside_pre: #not clean
+ data = unicodedata.normalize('NFC', data)
+
+ # Inside MathML – accumulate raw content
+ if self.inside_mathml:
+ self.mathml_raw += data
+ return
+
+ # Inside a KaTeX annotation – collect raw LaTeX
+ if self.inside_annotation:
+ _dbg('annotation DATA: ' + repr(data), self._log)
+ self.annotation_src += data
+ return
+
+ # Inside katex but not annotation – suppress rendered HTML noise
+ if self.inside_katex:
+ return
+
+ if self.inside_pre:
self.pre_data += data
else:
if self.inside_iframe:
space_removed_data = ""
else:
- #Put as a literal syntax of zim
+ # Escape Zim wiki syntax that might appear in pasted text
data = re.sub('(\'\'.+\'\')', r"''\1''", data)
data = re.sub('(\[\[.*\]\])', r"''\1''", data)
- data = re.sub('(^=+ .+ =+)', r"''\1''", data)
- data = re.sub('(^\t*\* )', r"''\1''", data)
- data = re.sub('(^\t*\[[ *x]\] )', r"''\1''", data)
+ data = re.sub(r'(^=+ .+ =+)', r"''\1''", data, flags=re.MULTILINE)
+ data = re.sub(r'(^\t*\* )', r"''\1''", data, flags=re.MULTILINE)
+ data = re.sub(r'(^\t*\[[ *x]\] )', r"''\1''", data, flags=re.MULTILINE)
data = re.sub('(\*\*.+\*\*)', r"''\1''", data)
data = re.sub('(\/\/.+\/\/)', r"''\1''", data)
data = re.sub('(__.+__)', r"''\1''", data)
data = re.sub('(~~.+~~)', r"''\1''", data)
data = re.sub('(\^\{.+\})', r"''\1''", data)
data = re.sub('(\_\{.+\})', r"''\1''", data)
- #If we are in a span tag, rstrip does not apply
+ # Escape Zim horizontal rule syntax (5+ dashes on a line)
+ data = re.sub(r'^-{5,}$', r"''\g<0>''", data, flags=re.MULTILINE)
if self.inside_span:
- space_removed_data = re.sub(r"[\s]+", " ", data)
+ space_removed_data = re.sub(r"[\s]+", " ", data)
else:
space_removed_data = re.sub(r"[\s]+", " ", data.rstrip())
- #If we are on a table they escape \ n and |
- if self.inside_table:
- space_removed_data = space_removed_data.replace("|", "\|")
- space_removed_data = space_removed_data.replace("\n", "\\n")
+ if self.inside_table:
+ space_removed_data = space_removed_data.replace("|", "\|")
+ space_removed_data = space_removed_data.replace("\n", "\\n")
self.zim_str += space_removed_data
def handle_entityref(self, name):
if name in name2codepoint:
- c = chr(name2codepoint[name])
+ c = unicodedata.normalize('NFC', chr(name2codepoint[name]))
+ else:
+ return
+ if self.inside_katex:
+ return
if self.inside_pre:
- #Add blank when tag not start line
if self.pre_data.endswith(("\n", "(", "[", "\t", "\"", " ", "/", '\xa0')):
blank = ""
else:
blank = " "
self.pre_data += blank + c
else:
- #Add blank when tag not start line
if self.zim_str.endswith(("\n", "(", "[", "\t", "\"", " ", "/", '\xa0')):
blank = ""
else:
@@ -416,7 +795,9 @@ def handle_charref(self, name):
c = chr(int(name[1:], 16))
else:
c = chr(int(name))
- #Add blank when tag not start line
+ c = unicodedata.normalize('NFC', c)
+ if self.inside_katex:
+ return
if self.zim_str.endswith(("\n", "(", "[", "\t", "\"", " ", "/", '\xa0')):
blank = ""
else:
@@ -424,6 +805,15 @@ def handle_charref(self, name):
self.zim_str += blank + c
def handle_startendtag(self, tag, attrs):
+ # Inside MathML – accumulate self-closing tags as raw
+ if self.inside_mathml:
+ self.mathml_raw += self._rebuild_tag(tag, attrs)[:-1] + '/>'
+ return
+
+ # ---- KaTeX self-closing tags – just track depth ----
+ if self.inside_katex:
+ return
+
if tag == "br":
self.zim_str += "\n\n"
elif tag == "input":
@@ -441,10 +831,8 @@ def handle_startendtag(self, tag, attrs):
if alt is None:
alt = "Image"
if src != "#" and not self.inside_table:
- #If the image and the link match, only the image remains and the label is deleted
if self.inside_tag == "a" and src == self.a_href:
self.zim_str = self.zim_str[:len(self.zim_str)-len("[[" + self.a_href + "|")]
- #If img inside a an <> image then prevails a if not image_inside_a
if self.inside_tag == "a" and src != self.a_href:
if self.image_inside_a:
self.zim_str = self.zim_str[:len(self.zim_str)-len("[[" + self.a_href + "|")]
@@ -464,10 +852,14 @@ def handle_startendtag(self, tag, attrs):
else:
self.zim_str += "[[{0}|{1}]]".format(src, alt)
elif tag == "hr":
- self.zim_str += "-----\n"
+ # hr is self-closing – handle here only, NOT in beg/end dicts
+ self.zim_str += "\n-----\n"
def to_zim(self, html_str, folder):
self.folder = folder
+ _dbg('=== to_zim called, folder=' + str(folder), self._log)
self.feed(html_str)
- #return self.zim_str.strip() + ("\n\n" if self.zim_str.strip().endswith("|") else "")
- return re.sub(r'\n\n+', "\n\n", self.zim_str).strip() + ("\n\n" if self.zim_str.strip().endswith("|") else "")
+ _dbg('=== feeding done, zim_str length=' + str(len(self.zim_str)), self._log)
+ result = re.sub(r'\n\n+', "\n\n", self.zim_str).strip() + ("\n\n" if self.zim_str.strip().endswith("|") else "")
+ _show_debug_terminal(self._log)
+ return result