feat: export translateMagicBlocks helper#1473
Conversation
eaglethrost
left a comment
There was a problem hiding this comment.
Good start! Using the magic block tokenizer & transformer is definitely the right call. The main thing is I think the serialisation code can be made simpler & we won't need to create as this much helper functions, see my comments!
|
|
||
| type AttributeValue = boolean | number | string; | ||
|
|
||
| interface SerializableNode { |
There was a problem hiding this comment.
- This interface seems to be only dealing with Images, so the name sounds too general
- Look into using the types we've defined, such as in https://github.com/readmeio/markdown/blob/next/processor/transform/mdxish/magic-blocks/types.ts
There was a problem hiding this comment.
good call, i've switched to the existing types so that image case uses MagicBlockImage directly and the generic walker uses MdastNode
| */ | ||
| export default function translateMagicBlocks(content: string) { | ||
| MAGIC_BLOCK_REGEX.lastIndex = 0; | ||
| return content.replace(MAGIC_BLOCK_REGEX, match => translateImageBlock(match)); |
There was a problem hiding this comment.
I'm curious on how we can make this scalable for other magic blocks. If we want to translate more types, is it as simple as to just extend this to include more translators?
There was a problem hiding this comment.
yep that's the shape now. there's a translators map keyed by block type, so a new one is a function plus one line to register it. each translator still owns its own parse + validate + AST construction since the block shapes are all different
| return image ? serializeImage(image, caption || undefined) : null; | ||
| } | ||
|
|
||
| function serializeTransformedNode(node: SerializableNode): string | null { |
There was a problem hiding this comment.
I think we're doing too much manual serialization. I wonder if you can just directly use the readmeToMdx function, or maybe mdxishMdastToMd.
So instead of manual serializers, you can extend the processor to use more transformers & stringify-ers to convert the image MDAST back to string.
There was a problem hiding this comment.
i've moved off the string templating, we're building the mdxJsxFlowElement and letting mdxishMdastToMd do the actual stringify pass. didn't pull in the full readmeToMdx since this path only needs the image branch, but the serializer is the same one now
Theskrtnerd
left a comment
There was a problem hiding this comment.
Other than what Dimas said, code lgtm. But just commenting since I dont have much context about this codebase
maximilianfalco
left a comment
There was a problem hiding this comment.
did a rough skim of things but just wondering if it would be better to just generalise this to support all magic block types anyway? the magic block tokenizer and transformer already handles this so i dont think itll be big to extend this a bit more
iirc the main reason we are doing this is because image magic block syntax was not known generally and Image JSX was more well known. wont this scenario also come up eventually when we want to "read" other magic block types? wdyt? @eaglethrost @RohanADev01
There was a problem hiding this comment.
Thanks for iterating, looks better!
wondering if it would be better to just generalise this to support all magic block types anyway?
-
I agree with @maximilianfalco on this! I don't mind having this PR focus on image first, but this should be explained in the PR desc, and also the code should be extensible & designed with this in mind (already good in that regard, maybe add more comments about it)
-
@maximilianfalco Looked through the code and it's not possible to get serialised JSX syntax if the nodes are not mdxFlow / Text elements, so Rohan's transformers make sense here. We're able to do it in the editor because of the pmToMdast code, which also manually creates JSX syntax. I think you should put a comment to explain this @RohanADev01 on what the to mdx transformers are for.
-
Also have a comment about code structure & cleanliness!
There was a problem hiding this comment.
I think this file mixes a lot of different code & use cases, so I think we should split up this file & move the utilities, transformers elsewhere. One idea is to have a lib/translate folder, under which you can have various block translators, utility functions, etc. Currently it's a bit confusing to read this
There was a problem hiding this comment.
yea or maybe you can just create a general translator class for magic blocks and do like a decorator pattern for each magic block type
There was a problem hiding this comment.
split into lib/translate/ as suggested. translateMagicBlocks.ts is the entry point + dispatcher now, image translator lives in lib/translate/image.ts, shared helpers in lib/translate/utils.ts.
There was a problem hiding this comment.
went with a translators map keyed by block type instead of a base class with decorators. each block has its own parse / validate / build so a base class would mostly be a hollow contract, but happy to revisit if we add more block types and start repeating ourselves.
|
|
||
| type MagicBlockImageNode = MagicBlockImage & MdastNode; | ||
|
|
||
| const MAGIC_BLOCK_OPEN_RE = /^\[block:([^\]]{1,100})\]/; |
There was a problem hiding this comment.
I know this regex is part of the MAGIC_BLOCK_REGEX, can you add a comment mentioning that?
There was a problem hiding this comment.
added a one-liner at lib/translate/translateMagicBlocks.ts:12 calling out this is a prefix subset of MAGIC_BLOCK_REGEX, with a cross-ref to lib/utils/extractMagicBlocks.ts:14.
maximilianfalco
left a comment
There was a problem hiding this comment.
overall nice! just some nits and questions. thanks heaps for iterating on this
There was a problem hiding this comment.
yea or maybe you can just create a general translator class for magic blocks and do like a decorator pattern for each magic block type
|
|
||
| function translateMagicBlock(raw: string) { | ||
| const blockType = raw.match(MAGIC_BLOCK_OPEN_RE)?.[1]; | ||
| const translator = blockType ? translators[blockType] : undefined; |
There was a problem hiding this comment.
i love this, thanks!
Splits the single-file translateMagicBlocks helper into a four-file module under lib/translate/ with a per-type translator registry, per PR review feedback from @eaglethrost and @maximilianfalco on #1473. Adds JSDoc on the public entry and the image translator, plus a comment noting MAGIC_BLOCK_OPEN_RE is a prefix subset of MAGIC_BLOCK_REGEX in lib/utils/extractMagicBlocks.ts. Adding a new block-type translator is now one new file plus one line in the translators map. No behavior change for the image case; serializer (mdxishMdastToMd + extended toAttributes) is unchanged.
Reverts an unintended type change in #1473 that flipped MagicBlockImage.data.hProperties.border from string to boolean. @maximilianfalco flagged the blast radius on the magic-blocks types file; reverting here keeps this PR scoped to the translateMagicBlocks helper. The runtime in magic-block-transformer.ts actually emits a boolean, so the upstream type is unsound, but cleaning that up belongs in a separate focused PR. lib/translate/image.ts now coerces hProperties.border locally through an unknown cast.
…gex isolation, transformer fallback cases
Moves tests to __tests__/lib/translate/ to mirror the new module path
and extends coverage:
- Golden toBe assertion on a canonical Akamai-shaped fixture so any
future serializer drift is caught byte-exact.
- border={false} preservation after the upstream border-type revert.
- Two image blocks in one document.
- Mixed image / unsupported / image in one document.
- [block:table] and [block:tutorial-tile] passthrough cases.
- MAGIC_BLOCK_REGEX.lastIndex isolation (pre-set and assert correct
output, also assert subsequent extractMagicBlocks unaffected).
- Behavioral registry test through the public entry, no internal-map
import.
- Synthetic fixture where hasImageBlockData passes but the transformer
produces no usable node, asserting raw passthrough.
maximilianfalco
left a comment
There was a problem hiding this comment.
looks good to me! deferring final approval to @eaglethrost and @kevinports
eaglethrost
left a comment
There was a problem hiding this comment.
Thanks for iterating, lgtm as well! Wait till kevin or another readme eng approves before merging 🙏
xavierandueza
left a comment
There was a problem hiding this comment.
Nice work! LGTM, but have limited context as compared to others.
Summary
translateMagicBlocksfrom@readme/markdownso consumers (e.g. gitto's AI lint preprocessor) can translate legacy[block:image]syntax into<Image />JSX without vendoring their own magic-block parser.[block:image]blocks are translated; all other magic block types and malformed blocks pass through unchanged.Changes
New helper module
lib/translateMagicBlocks.ts: drives the existing extractor → tokenizer → transformer pipeline for[block:image]spans only, then serializes the transformed MDAST into<Image />JSX. Captioned and uncaptioned arms are both handled; the uncaptioned arm preservesalign/border/widththatmdast-util-to-markdown's defaultimagehandler would otherwise drop.",\,<,>,{,}, control chars) are emitted in JSX-expression-attribute form (key={JSON.stringify(value)}) so the output is parseable MDX.safeMode: trueinternally so<<variables>>and{user.*}round-trip as literal text.Public export
lib/index.ts: re-exports the helper adjacent tostripComments, matching the existing barrel convention.Tests
__tests__/lib/translateMagicBlocks.test.ts: covers captioned/uncaptioned image translation, attribute preservation, first-image-wins, line-count preservation, malformed-JSON fall-through, unknown-block-type pass-through, and MDX-ambiguous attribute escaping.Customer Impact
Internal/library-only. This PR is purely additive — it ships a new named export with no changes to existing behavior. Customer-facing impact lands when downstream consumers (gitto) swap their vendored helper for this export in a follow-up PR.
QA & Testing
npm test -- translateMagicBlockspassestranslateMagicBlocksand confirm[block:image]content withalign/border/widthround-trips to<Image />JSX with attributes preserved[block:callout],[block:embed], etc.) and malformed[block:image]JSON pass through unchanged