⚡ MyST Markdown 实时预览

⚡ MyST Markdown 实时预览#

这是 MyST Markdown docutils 渲染器的实时预览

你可以在下方编辑文本/配置,并查看实时输出。

"""live_preview — 在浏览器中实时预览 MyST 文档 基于 Docutils 与 MyST-Parser,将输入的 MyST 文本转换为 HTML 或其它格式, 并在 PyScript/Pyodide 环境中通过 DOM 事件实现即时刷新。 - 覆盖简化的 HTML 翻译器与 Writer,仅输出 body 内容并禁用样式注入; - 解析 YAML 配置以设置 Docutils 转换参数,收集并显示警告信息; - 通过页面输入与选择器,实时触发转换并更新预览区域。 """ import traceback from io import StringIO import yaml from docutils.core import publish_string from docutils.frontend import filter_settings_spec from docutils.writers.html5_polyglot import HTMLTranslator, Writer from js import window, document from dataclasses import dataclass from myst_parser import __version__ from myst_parser.parsers.docutils_ import Parser HTML_FORMATS = {"html", "html4", "html5", "html5_polyglot"} def is_html_format(name: str) -> bool: try: return any(fmt in (name or "").lower() for fmt in HTML_FORMATS) except Exception: return False PARSER = Parser() MIN_INTERVAL_MS = 250 _last_run_ms = 0.0 _last_config = "" _last_input = "" _last_format = "" class SimpleTranslator(HTMLTranslator): """简化的 HTML 翻译器。 - 为代码块(literal_block)追加 ``highlight`` 类,便于外部样式统一高亮; - 禁用样式表插入,避免在页面中重复注入样式。 """ def visit_literal_block(self, node): """处理代码块节点,追加类名后交由父类渲染。""" node["classes"].append("highlight") return super().visit_literal_block(node) def stylesheet_call(self, *args, **kwargs): """覆盖样式表插入逻辑,返回空字符串以禁用样式输出。""" return "" class SimpleWriter(Writer): """简化的 HTML Writer,仅输出正文内容。 - 通过 ``filter_settings_spec`` 精简可用设置; - 在 ``apply_template`` 中返回 ``{body}``,避免包裹多余模板结构。 """ settings_spec = filter_settings_spec( Writer.settings_spec, "template", ) def apply_template(self): """使用插值字典仅渲染 `body` 段并返回字符串。""" subs = self.interpolation_dict() return "{body}\n".format(**subs) def __init__(self): """初始化写入器,设置部件存储与翻译器类型。""" self.parts = {} self.translator_class = SimpleTranslator def convert(input_config: str, input_myst: str, writer_name: str) -> dict: """将 MyST 文本转换为指定输出格式。 Args: input_config: YAML 字符串形式的 Docutils 设置(可为空)。 input_myst: 待转换的 MyST 文本内容。 writer_name: 输出目标;包含 ``html`` 时使用 ``SimpleWriter``, 否则传递给 Docutils 的 ``writer_name``。 Returns: dict: 包含 ``output``(转换结果字符串)与 ``warnings``(警告文本)。 Notes: - 配置解析失败时会记录错误并回退到空设置; - 通过 ``publish_string`` 执行转换,异常会被捕获并返回堆栈信息。 """ warning_stream = StringIO() try: settings = yaml.safe_load(input_config) if input_config else {} assert isinstance(settings, dict), "not a dictionary" except Exception as exc: warning_stream.write(f"ERROR: config load: {exc}\n") settings = {} settings.update( { "output_encoding": "unicode", "warning_stream": warning_stream, # to mimic the sphinx parser "doctitle_xform": False, "sectsubtitle_xform": False, "initial_header_level": 1, } ) try: output = publish_string( input_myst, parser=PARSER, settings_overrides=settings, **( {"writer": SimpleWriter()} if is_html_format((writer_name or "").lower()) else {"writer_name": writer_name} ), ) except Exception as exc: output = f"ERROR: conversion:\n{exc}\n{traceback.format_exc()}" return {"output": output, "warnings": warning_stream.getvalue()} # 使用 dataclass 统一管理 DOM 引用 @dataclass class LivePreviewDOM: version_label: object = None config_textarea: object = None input_textarea: object = None output_iframe: object = None output_raw: object = None warnings_textarea: object = None oformat_select: object = None DOM = LivePreviewDOM() def do_convert(event=None): """读取页面输入并触发转换,更新预览与原始输出。 - 当输出格式包含 `html` 时,将 HTML 注入到预览容器; - 否则提示切换到 HTML 以查看渲染结果; - 同步展示转换产生的警告信息。 """ global _last_run_ms, _last_config, _last_input, _last_format # Ensure required DOM elements exist if not all([DOM.config_textarea, DOM.input_textarea, DOM.output_raw, DOM.output_iframe, DOM.warnings_textarea, DOM.oformat_select]): try: window.console.warn("live_preview: missing required DOM elements") except Exception: pass return curr_config = DOM.config_textarea.value or "" curr_input = DOM.input_textarea.value or "" curr_format = DOM.oformat_select.value or "" changed = (curr_config != _last_config) or (curr_input != _last_input) or (curr_format != _last_format) # Simple throttle to avoid excessive conversions during rapid typing try: now = window.performance.now() except Exception: now = 0 if changed and (now - _last_run_ms < MIN_INTERVAL_MS): return result = convert(curr_config, curr_input, curr_format) _last_run_ms = now _last_config, _last_input, _last_format = curr_config, curr_input, curr_format DOM.output_raw.value = result["output"] if is_html_format(curr_format): DOM.output_iframe.innerHTML = result["output"] else: DOM.output_iframe.textContent = "Change output format to HTML to see output" DOM.warnings_textarea.value = result["warnings"] def setup_dom(): """初始化浏览器端的 DOM 引用与事件绑定,并进行首轮转换。""" # 通过 document 查询并缓存需要的元素到 DOM 容器 DOM.version_label = document.querySelector("span#myst-version") DOM.config_textarea = document.querySelector("textarea#input_config") DOM.input_textarea = document.querySelector("textarea#input_myst") DOM.output_iframe = document.querySelector("div#output_html") DOM.output_raw = document.querySelector("textarea#output_raw") DOM.warnings_textarea = document.querySelector("textarea#output_warnings") DOM.oformat_select = document.querySelector("select#output_format") # 设置版本标签与事件处理 if DOM.version_label: DOM.version_label.textContent = f"myst-parser v{__version__}" if DOM.config_textarea: DOM.config_textarea.oninput = do_convert if DOM.input_textarea: DOM.input_textarea.oninput = do_convert if DOM.oformat_select: DOM.oformat_select.onchange = do_convert # 首次渲染 do_convert() # 执行初始化封装 setup_dom()
myst-parser v
{ "splashscreen": { "autoclose": true }, "packages": [ "myst-docutils==4.0", "docutils==0.20", "pygments" ] }