-
Notifications
You must be signed in to change notification settings - Fork 21
Add Markdown text escaping #166
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
Changes from 12 commits
462a0e3
94eab8f
77d140a
ebb2727
3f73097
3b7c5c3
ae20077
e4590c7
f33476f
564beeb
4501a9a
7802fe6
ae50109
d0d51f1
ccdd0a5
fb03df0
f78cadc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,6 +3,11 @@ | |||||||||||||||||||||||||||||||||||||||
| from html import escape | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| from draftjs_exporter.engines.base import Attr, DOMEngine | ||||||||||||||||||||||||||||||||||||||||
| from draftjs_exporter.markdown.escape import ( | ||||||||||||||||||||||||||||||||||||||||
| code_block_fence, | ||||||||||||||||||||||||||||||||||||||||
| code_span_delimiters, | ||||||||||||||||||||||||||||||||||||||||
| escape_text, | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| from draftjs_exporter.types import HTML, Tag | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| # http://w3c.github.io/html/single-page.html#void-elements | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -28,7 +33,9 @@ | |||||||||||||||||||||||||||||||||||||||
| class Elt: | ||||||||||||||||||||||||||||||||||||||||
| """A DOM element that the Markdown engine manipulates. | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| Identical to the string engine's Elt, but rendering does not escape text. | ||||||||||||||||||||||||||||||||||||||||
| Identical to the string engine's Elt, with extra Markdown-specific node | ||||||||||||||||||||||||||||||||||||||||
| types: ``mark_safe`` (structural syntax rendered verbatim), ``code_span`` | ||||||||||||||||||||||||||||||||||||||||
| and ``code_block`` (content rendered unescaped with sized delimiters). | ||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| __slots__ = ("type", "attr", "children", "markup") | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -60,7 +67,12 @@ def from_html(markup: HTML) -> "Elt": | |||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| class DOMMarkdown(DOMEngine): | ||||||||||||||||||||||||||||||||||||||||
| """String concatenation implementation of the DOM API for Markdown output.""" | ||||||||||||||||||||||||||||||||||||||||
| """String concatenation implementation of the DOM API for Markdown output. | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| Invariant: a plain ``str`` child in the tree is always user-controlled | ||||||||||||||||||||||||||||||||||||||||
| text. Structural Markdown syntax must be wrapped in ``mark_safe`` | ||||||||||||||||||||||||||||||||||||||||
| elements by components, or it will be escaped as text. | ||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| @staticmethod | ||||||||||||||||||||||||||||||||||||||||
| def create_tag(type_: Tag, attr: Attr | None = None) -> Elt: | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -118,17 +130,66 @@ def render_attrs(attr: Attr) -> str: | |||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| @staticmethod | ||||||||||||||||||||||||||||||||||||||||
| def render_children(children: list[HTML | Elt]) -> HTML: | ||||||||||||||||||||||||||||||||||||||||
| """Render a list of children to a string without escaping text. | ||||||||||||||||||||||||||||||||||||||||
| """Render a list of children, escaping plain strings as user text. | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| A plain ``str`` child is always user-controlled text and is escaped. | ||||||||||||||||||||||||||||||||||||||||
| Structural syntax wrapped in ``mark_safe`` elements renders verbatim. | ||||||||||||||||||||||||||||||||||||||||
| Line-start-sensitive characters are escaped when a string begins a | ||||||||||||||||||||||||||||||||||||||||
| line: at the start of the children list, when the output so far ends | ||||||||||||||||||||||||||||||||||||||||
| with a newline or carriage return, or right after a ``mark_safe`` | ||||||||||||||||||||||||||||||||||||||||
| element created with ``block_prefix``. | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| Parameters: | ||||||||||||||||||||||||||||||||||||||||
| children: A list of strings and elements to render. | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||||||||||||||
| The concatenated child content. | ||||||||||||||||||||||||||||||||||||||||
| The rendered children. | ||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||
| out: list[str] = [] | ||||||||||||||||||||||||||||||||||||||||
| at_line_start = True | ||||||||||||||||||||||||||||||||||||||||
| for c in children: | ||||||||||||||||||||||||||||||||||||||||
| if isinstance(c, Elt): | ||||||||||||||||||||||||||||||||||||||||
| rendered = DOMMarkdown.render(c) | ||||||||||||||||||||||||||||||||||||||||
| out.append(rendered) | ||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||
| c.type == "mark_safe" | ||||||||||||||||||||||||||||||||||||||||
| and c.attr | ||||||||||||||||||||||||||||||||||||||||
| and c.attr.get("block_prefix") == "true" | ||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||
| at_line_start = True | ||||||||||||||||||||||||||||||||||||||||
| elif rendered: | ||||||||||||||||||||||||||||||||||||||||
| at_line_start = rendered.endswith(("\n", "\r")) | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+154
to
+161
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Suggestion:
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||
| out.append(escape_text(c, at_line_start)) | ||||||||||||||||||||||||||||||||||||||||
| if c: | ||||||||||||||||||||||||||||||||||||||||
| at_line_start = c.endswith(("\n", "\r")) | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+162
to
+165
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Empty string children don't update at_line_start, causing consecutive empty strings to inherit stale state. This could lead to incorrect escaping for empty strings that should be at line start but aren't marked as such. Consider using 'if not c or c.endswith(('\n', '\r'))' to handle empty strings correctly (they don't change line start state but shouldn't break tracking). Suggestion:
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inheriting state after an empty string is intentional: nothing was emitted, so line-start state must not change. The suggested change would set at_line_start=True after empty strings mid-line (e.g. right after a "**" marker), producing false-positive escapes. Current behavior is correct. |
||||||||||||||||||||||||||||||||||||||||
| return "".join(out) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| @staticmethod | ||||||||||||||||||||||||||||||||||||||||
| def flatten_text(children: list["str | Elt"]) -> str: | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+168
to
+169
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Suggestion:
Suggested change
Comment on lines
+168
to
+169
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Suggestion:
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
| """Flatten children to raw text, discarding element structure. | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| ``mark_safe`` and ``escaped_html`` markup is included verbatim; | ||||||||||||||||||||||||||||||||||||||||
| other elements contribute only their text content. | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| Parameters: | ||||||||||||||||||||||||||||||||||||||||
| children: A list of strings and elements. | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||||||||||||||
| The concatenated raw text. | ||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||
| return "".join( | ||||||||||||||||||||||||||||||||||||||||
| [DOMMarkdown.render(c) if isinstance(c, Elt) else c for c in children] | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| parts = [] | ||||||||||||||||||||||||||||||||||||||||
| for c in children: | ||||||||||||||||||||||||||||||||||||||||
| if isinstance(c, Elt): | ||||||||||||||||||||||||||||||||||||||||
| if c.type == "mark_safe": | ||||||||||||||||||||||||||||||||||||||||
| parts.append(c.attr["markup"] if c.attr else "") | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+184
to
+185
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In flatten_text, the mark_safe branch accesses c.attr['markup'] without using .get() for safe access. If c.attr is None, the guard catches it. But if c.attr is a dict missing the 'markup' key (possible with direct create_element calls), KeyError is raised. Use .get("markup", "") for consistency with the code_block case. Suggestion:
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Deliberate design choice: a mark_safe element without a markup key is a component contract violation, and a loud KeyError surfaces it immediately at render time. The suggested .get("markup", "") would silently drop structural syntax from the output, turning a developer bug into invisible output corruption. Loud beats silent here.
Comment on lines
+184
to
+185
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Suggestion:
Suggested change
Comment on lines
+184
to
+185
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential Suggestion:
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
| elif c.markup: | ||||||||||||||||||||||||||||||||||||||||
| parts.append(c.markup) | ||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||
| parts.append(DOMMarkdown.flatten_text(c.children)) | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+188
to
+189
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Suggestion:
Suggested change
Comment on lines
+188
to
+189
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Suggestion:
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||
| parts.append(c) | ||||||||||||||||||||||||||||||||||||||||
| return "".join(parts) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| @staticmethod | ||||||||||||||||||||||||||||||||||||||||
| def render(elt: Elt) -> HTML: | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -150,8 +211,19 @@ def render(elt: Elt) -> HTML: | |||||||||||||||||||||||||||||||||||||||
| match type_: | ||||||||||||||||||||||||||||||||||||||||
| case "fragment": | ||||||||||||||||||||||||||||||||||||||||
| return children | ||||||||||||||||||||||||||||||||||||||||
| case "mark_safe": | ||||||||||||||||||||||||||||||||||||||||
| return elt.attr["markup"] if elt.attr else "" | ||||||||||||||||||||||||||||||||||||||||
| case "escaped_html": | ||||||||||||||||||||||||||||||||||||||||
| return elt.markup | ||||||||||||||||||||||||||||||||||||||||
| case "code_span": | ||||||||||||||||||||||||||||||||||||||||
| content = DOMMarkdown.flatten_text(elt.children) | ||||||||||||||||||||||||||||||||||||||||
| opening, closing = code_span_delimiters(content) | ||||||||||||||||||||||||||||||||||||||||
| return f"{opening}{content}{closing}" | ||||||||||||||||||||||||||||||||||||||||
| case "code_block": | ||||||||||||||||||||||||||||||||||||||||
| content = DOMMarkdown.flatten_text(elt.children) | ||||||||||||||||||||||||||||||||||||||||
| fence_char = (elt.attr or {}).get("fence", "`") | ||||||||||||||||||||||||||||||||||||||||
| fence = code_block_fence(content, fence_char) | ||||||||||||||||||||||||||||||||||||||||
| return f"{fence}\n{content}{fence}\n\n" | ||||||||||||||||||||||||||||||||||||||||
| case _ if type_ in VOID_ELEMENTS: | ||||||||||||||||||||||||||||||||||||||||
| return f"<{type_}{attr}/>" | ||||||||||||||||||||||||||||||||||||||||
| case _: | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -35,7 +35,7 @@ | |||||||||
| entity_fallback, | ||||||||||
| style_fallback, | ||||||||||
| ) | ||||||||||
| from draftjs_exporter.markdown.styles import inline_style | ||||||||||
| from draftjs_exporter.markdown.styles import code_span, inline_style | ||||||||||
| from draftjs_exporter.types import Component, ConfigMap | ||||||||||
|
|
||||||||||
|
|
||||||||||
|
|
@@ -72,7 +72,7 @@ class MarkdownOptions(TypedDict, total=False): | |||||||||
| "element": ol, | ||||||||||
| "wrapper": list_wrapper, | ||||||||||
| }, | ||||||||||
| BLOCK_TYPES.BLOCKQUOTE: prefixed_block("> "), | ||||||||||
| BLOCK_TYPES.BLOCKQUOTE: prefixed_block("> ", block_prefix=True), | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Suggestion:
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Already the case as written, and the nested-construct behavior is pinned by test_line_start_in_blockquote in tests/markdown/test_escaping.py. |
||||||||||
| BLOCK_TYPES.CODE: { | ||||||||||
| "element": code_element, | ||||||||||
| "wrapper": code_wrapper, | ||||||||||
|
|
@@ -85,7 +85,7 @@ class MarkdownOptions(TypedDict, total=False): | |||||||||
| HTML_STYLE_MAP, | ||||||||||
| **{ | ||||||||||
| INLINE_STYLES.BOLD: inline_style("**"), | ||||||||||
| INLINE_STYLES.CODE: inline_style("`"), | ||||||||||
| INLINE_STYLES.CODE: code_span, | ||||||||||
| INLINE_STYLES.ITALIC: inline_style("_"), | ||||||||||
| INLINE_STYLES.STRIKETHROUGH: inline_style("~"), | ||||||||||
| INLINE_STYLES.FALLBACK: style_fallback, | ||||||||||
|
|
@@ -147,9 +147,9 @@ def build_markdown_config(options: MarkdownOptions | None = None) -> ExporterCon | |||||||||
| "element": make_ol(ol_delimiter), | ||||||||||
| "wrapper": list_wrapper, | ||||||||||
| }, | ||||||||||
| BLOCK_TYPES.BLOCKQUOTE: prefixed_block("> "), | ||||||||||
| BLOCK_TYPES.BLOCKQUOTE: prefixed_block("> ", block_prefix=True), | ||||||||||
| BLOCK_TYPES.CODE: { | ||||||||||
| "element": make_code_element(fence), | ||||||||||
| "element": make_code_element(), | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||||||||||
| "wrapper": make_code_wrapper(fence), | ||||||||||
|
Comment on lines
+152
to
153
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Suggestion:
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This suggestion matches the code as written — nothing to change. |
||||||||||
| }, | ||||||||||
| BLOCK_TYPES.ATOMIC: lambda props: props["children"], | ||||||||||
|
|
@@ -158,7 +158,7 @@ def build_markdown_config(options: MarkdownOptions | None = None) -> ExporterCon | |||||||||
| style_map: ConfigMap = { | ||||||||||
| **HTML_STYLE_MAP, | ||||||||||
| INLINE_STYLES.BOLD: inline_style(bold), | ||||||||||
| INLINE_STYLES.CODE: inline_style("`"), | ||||||||||
| INLINE_STYLES.CODE: code_span, | ||||||||||
| INLINE_STYLES.ITALIC: inline_style(italic), | ||||||||||
| INLINE_STYLES.STRIKETHROUGH: inline_style(strikethrough), | ||||||||||
| } | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,20 +1,24 @@ | ||||||
| """Markdown block-level components: headings, lists, blockquotes, and plain paragraphs.""" | ||||||
|
|
||||||
| from draftjs_exporter.markdown.helpers import block, inline | ||||||
| from draftjs_exporter.markdown.helpers import block, inline, mark_safe | ||||||
| from draftjs_exporter.markdown.lists import list_item, make_numbered_li_prefix | ||||||
| from draftjs_exporter.types import Component, Element, Props | ||||||
|
|
||||||
|
|
||||||
| def prefixed_block(prefix: str) -> Component: | ||||||
| def prefixed_block(prefix: str, block_prefix: bool = False) -> Component: | ||||||
| """Create a block component that prefixes its children with the given string. | ||||||
|
|
||||||
| Parameters: | ||||||
| prefix: The literal prefix to insert before the block's children. | ||||||
| block_prefix: Whether children can start a nested block after the | ||||||
| prefix (true for blockquotes, false for headings). | ||||||
|
|
||||||
| Returns: | ||||||
| A component that renders the prefixed block. | ||||||
| """ | ||||||
| return lambda props: block([prefix, props["children"]]) | ||||||
| return lambda props: block( | ||||||
| [mark_safe(prefix, block_prefix=block_prefix), props["children"]] | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Type mismatch: Suggestion:
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would be a real bug if attributes weren't stringified, but DOM.create_element coerces True to the string "true" before create_tag sees it, and the behavior is pinned end-to-end by test_line_start_in_list_item and test_line_start_in_blockquote in tests/markdown/test_escaping.py (both fail if the comparison stops matching). Not a defect. |
||||||
| ) | ||||||
|
|
||||||
|
|
||||||
| def make_ul(marker: str) -> Component: | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,26 +1,22 @@ | ||||||||||||||
| """Markdown code block components and fenced code builders.""" | ||||||||||||||
|
|
||||||||||||||
| from draftjs_exporter.dom import DOM | ||||||||||||||
| from draftjs_exporter.markdown.lists import get_li_suffix | ||||||||||||||
| from draftjs_exporter.markdown.helpers import mark_safe | ||||||||||||||
| from draftjs_exporter.types import Component, Element, Props | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def make_code_element(fence: str) -> Component: | ||||||||||||||
| """Create a code-block element component using the given fence. | ||||||||||||||
| def make_code_element() -> Component: | ||||||||||||||
| """Create a code-block line component. | ||||||||||||||
|
|
||||||||||||||
| Parameters: | ||||||||||||||
| fence: The delimiter to use at the closing of the code block. | ||||||||||||||
| Each Draft.js code block contributes one line to the shared code_block | ||||||||||||||
| node created by the wrapper. | ||||||||||||||
|
|
||||||||||||||
| Returns: | ||||||||||||||
| A component that renders the contents and closing fence of a code block. | ||||||||||||||
| A component that renders one line of code block content. | ||||||||||||||
| """ | ||||||||||||||
|
|
||||||||||||||
| def element(props: Props) -> Element: | ||||||||||||||
| suffix = get_li_suffix(props) | ||||||||||||||
| block_end = f"\n{fence}" if suffix == "\n\n" else "" | ||||||||||||||
| return DOM.create_element( | ||||||||||||||
| "fragment", {}, [props["children"], block_end, suffix] | ||||||||||||||
| ) | ||||||||||||||
| return DOM.create_element("fragment", {}, [props["children"], mark_safe("\n")]) | ||||||||||||||
|
|
||||||||||||||
| return element | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -29,14 +25,15 @@ def make_code_wrapper(fence: str) -> Component: | |||||||||||||
| """Create a code-block wrapper component using the given fence. | ||||||||||||||
|
|
||||||||||||||
| Parameters: | ||||||||||||||
| fence: The delimiter to place at the start of the code block. | ||||||||||||||
| fence: The fence delimiter; its first character sizes the rendered | ||||||||||||||
| fence (`` ``` `` or ``~~~``). | ||||||||||||||
|
|
||||||||||||||
| Returns: | ||||||||||||||
| A component that renders only the opening fence. | ||||||||||||||
| A component that creates the code_block node holding all lines. | ||||||||||||||
| """ | ||||||||||||||
| prefix = f"{fence}\n" | ||||||||||||||
| return lambda props: DOM.create_element("fragment", {}, [prefix]) | ||||||||||||||
| fence_char = fence[0] | ||||||||||||||
| return lambda props: DOM.create_element("code_block", {"fence": fence_char}) | ||||||||||||||
|
Comment on lines
+34
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Accessing Suggestion:
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Not a defect: fence is typed as a literal set of the two supported fences in build_markdown_config, so type-checked callers cannot pass an empty string, and a runtime misuse fails loudly (IndexError) rather than silently. Per docs/SECURITY.md the exporter config is trusted developer code, not attacker input. Adding a guard + test to rephrase an already-loud failure is not worth it. |
||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| code_element: Component = make_code_element("```") | ||||||||||||||
| code_element: Component = make_code_element() | ||||||||||||||
| code_wrapper: Component = make_code_wrapper("```") | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| """Markdown entity decorators for images, links, and horizontal rules.""" | ||
|
|
||
| from draftjs_exporter.markdown.helpers import block, inline | ||
| from draftjs_exporter.markdown.helpers import block, inline, link_destination, mark_safe | ||
| from draftjs_exporter.types import Component, Element, Props | ||
|
|
||
|
|
||
|
|
@@ -13,7 +13,15 @@ def image(props: Props) -> Element: | |
| Returns: | ||
| A block-level image element. | ||
| """ | ||
| return block([""]) | ||
| return block( | ||
| [ | ||
| mark_safe(", | ||
| link_destination(props["src"]), | ||
| mark_safe(")"), | ||
| ] | ||
| ) | ||
|
|
||
|
|
||
| def link(props: Props) -> Element: | ||
|
|
@@ -25,7 +33,15 @@ def link(props: Props) -> Element: | |
| Returns: | ||
| An inline link element. | ||
| """ | ||
| return inline(["[", props["children"], "](", props["url"], ")"]) | ||
| return inline( | ||
| [ | ||
| mark_safe("["), | ||
| props["children"], | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| mark_safe("]("), | ||
| link_destination(props["url"]), | ||
| mark_safe(")"), | ||
| ] | ||
| ) | ||
|
Comment on lines
+36
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
|
|
||
|
|
||
| def make_horizontal_rule(marker: str) -> Component: | ||
|
|
@@ -37,7 +53,7 @@ def make_horizontal_rule(marker: str) -> Component: | |
| Returns: | ||
| A component that renders a horizontal rule. | ||
| """ | ||
| return lambda props: block([marker]) | ||
| return lambda props: block([mark_safe(marker)]) | ||
|
|
||
|
|
||
| horizontal_rule: Component = make_horizontal_rule("---") | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing integration tests for
block_prefixbehavior with nested blocks (e.g., heading inside blockquote, list item with heading-like text). Therender_childrenlogic is correct but untested at integration level.Suggestion: