Skip to content

Commit d583493

Browse files
committed
fix: normalize MDX output and pin release tooling
1 parent 1a1ee73 commit d583493

7 files changed

Lines changed: 112 additions & 111 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
runs-on: ubuntu-24.04
1616
steps:
1717
- uses: actions/checkout@v7
18-
- uses: pnpm/action-setup@v4
18+
- uses: pnpm/action-setup@v4.4.0
1919
with:
2020
version: 11.20.0
2121
- uses: actions/setup-node@v7

.github/workflows/release.yml

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,14 @@ jobs:
4848
target: aarch64-unknown-linux-musl
4949
platform: linux-arm64-musl
5050
flags: -x
51+
zig_arch: aarch64
52+
zig_sha256: f7a654acc967864f7a050ddacfaa778c7504a0eca8d2b678839c21eea47c992b
5153
- os: ubuntu-24.04
5254
target: x86_64-unknown-linux-musl
5355
platform: linux-x64-musl
5456
flags: -x
57+
zig_arch: x86_64
58+
zig_sha256: 24aeeec8af16c381934a6cd7d95c807a8cb2cf7df9fa40d359aa884195c4716c
5559
- os: windows-2025
5660
target: x86_64-pc-windows-msvc
5761
platform: win32-x64-msvc
@@ -60,7 +64,7 @@ jobs:
6064
timeout-minutes: 30
6165
steps:
6266
- uses: actions/checkout@v7
63-
- uses: pnpm/action-setup@v4
67+
- uses: pnpm/action-setup@v4.4.0
6468
with:
6569
version: 11.20.0
6670
- uses: actions/setup-node@v7
@@ -71,10 +75,17 @@ jobs:
7175
with:
7276
toolchain: 1.97.1
7377
targets: ${{ matrix.target }}
74-
- uses: mlugg/setup-zig@v2
78+
- name: Install Zig
7579
if: contains(matrix.target, 'musl')
76-
with:
77-
version: 0.14.1
80+
shell: bash
81+
run: |
82+
zig_archive="$RUNNER_TEMP/zig.tar.xz"
83+
zig_dir="$RUNNER_TEMP/zig"
84+
curl -fsSLo "$zig_archive" "https://ziglang.org/download/0.14.1/zig-${{ matrix.zig_arch }}-linux-0.14.1.tar.xz"
85+
echo "${{ matrix.zig_sha256 }} $zig_archive" | sha256sum -c -
86+
mkdir -p "$zig_dir"
87+
tar -xJf "$zig_archive" -C "$zig_dir" --strip-components=1
88+
echo "$zig_dir" >> "$GITHUB_PATH"
7889
- uses: taiki-e/install-action@v2
7990
if: contains(matrix.target, 'musl')
8091
with:
@@ -115,7 +126,7 @@ jobs:
115126
id-token: write
116127
steps:
117128
- uses: actions/checkout@v7
118-
- uses: pnpm/action-setup@v4
129+
- uses: pnpm/action-setup@v4.4.0
119130
with:
120131
version: 11.20.0
121132
- uses: actions/setup-node@v7

native/src/document.rs

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::Diagnostic;
1212
use crate::config::{
1313
NativeCollectionConfig, NativeMdxConfig, NativeMediaConfig, apply_schema_defaults_and_validate,
1414
};
15-
use crate::hast::{apply_hard_breaks, rewrite_media};
15+
use crate::hast::rewrite_media;
1616

1717
#[derive(Debug, serde::Serialize)]
1818
#[serde(rename_all = "camelCase")]
@@ -93,17 +93,17 @@ pub fn prepare_mdx(
9393
media: &NativeMediaConfig,
9494
) -> Result<PreparedMdx, Vec<Diagnostic>> {
9595
let options = mdx_options(file, config);
96-
let mdast = mdxjs::mdast_util_from_mdx(body, &options)
96+
let mut mdast = mdxjs::mdast_util_from_mdx(body, &options)
9797
.map_err(|error| vec![mdx_diagnostic(file, error)])?;
9898
let reading_words = reading_units(&mdast);
9999
let mut code_blocks = Vec::new();
100100
if highlight {
101101
collect_code_blocks(&mdast, document_id, &mut code_blocks);
102102
}
103-
let mut tree = mdxjs::mdast_util_to_hast(&mdast);
104103
if config.hard_breaks {
105-
apply_hard_breaks(&mut tree);
104+
apply_hard_breaks(&mut mdast);
106105
}
106+
let mut tree = mdxjs::mdast_util_to_hast(&mdast);
107107
let media = rewrite_media(&mut tree, root, Path::new(file), media)?;
108108

109109
Ok(PreparedMdx {
@@ -115,6 +115,37 @@ pub fn prepare_mdx(
115115
})
116116
}
117117

118+
fn apply_hard_breaks(node: &mut markdown::mdast::Node) {
119+
let Some(children) = node.children_mut() else {
120+
return;
121+
};
122+
let old_children = std::mem::take(children);
123+
for mut child in old_children {
124+
if let markdown::mdast::Node::Text(text) = &child
125+
&& (text.value.contains('\n') || text.value.contains('\r'))
126+
{
127+
let normalized = text.value.replace("\r\n", "\n").replace('\r', "\n");
128+
let parts = normalized.split('\n').collect::<Vec<_>>();
129+
for (index, part) in parts.iter().enumerate() {
130+
if !part.is_empty() {
131+
children.push(markdown::mdast::Node::Text(markdown::mdast::Text {
132+
value: (*part).into(),
133+
position: None,
134+
}));
135+
}
136+
if index + 1 < parts.len() {
137+
children.push(markdown::mdast::Node::Break(markdown::mdast::Break {
138+
position: None,
139+
}));
140+
}
141+
}
142+
continue;
143+
}
144+
apply_hard_breaks(&mut child);
145+
children.push(child);
146+
}
147+
}
148+
118149
fn reading_units(node: &markdown::mdast::Node) -> usize {
119150
match node {
120151
markdown::mdast::Node::Code(_) | markdown::mdast::Node::InlineCode(_) => 0,
@@ -228,7 +259,7 @@ fn react_style_object(style: &str) -> Option<Expr> {
228259
Some(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
229260
key: PropName::Str(Str {
230261
span: DUMMY_SP,
231-
value: name.into(),
262+
value: react_style_name(name).into(),
232263
raw: None,
233264
}),
234265
value: Box::new(Expr::Lit(Lit::Str(Str {
@@ -245,6 +276,28 @@ fn react_style_object(style: &str) -> Option<Expr> {
245276
}))
246277
}
247278

279+
fn react_style_name(name: &str) -> String {
280+
if name.starts_with("--") {
281+
return name.into();
282+
}
283+
let mut result = String::with_capacity(name.len());
284+
let mut uppercase_next = false;
285+
for character in name.chars() {
286+
if character == '-' {
287+
uppercase_next = true;
288+
} else if uppercase_next {
289+
result.extend(character.to_uppercase());
290+
uppercase_next = false;
291+
} else {
292+
result.push(character);
293+
}
294+
}
295+
if name.starts_with("-ms-") {
296+
result.replace_range(..1, "m");
297+
}
298+
result
299+
}
300+
248301
fn mdx_options(file: &str, config: &NativeMdxConfig) -> mdxjs::Options {
249302
let mut options = if config.gfm {
250303
mdxjs::Options::gfm()
@@ -404,6 +457,38 @@ mod tests {
404457
assert!(!module.contains("secret"));
405458
}
406459

460+
#[test]
461+
fn hard_breaks_only_replace_newlines_inside_text_nodes() {
462+
let source = "first\nsecond\n\n# Heading\n\n| a | b |\n| - | - |\n| 1 | 2 |\n";
463+
let options = NativeMdxConfig {
464+
gfm: true,
465+
hard_breaks: true,
466+
jsx_import_source: "react".into(),
467+
provider_import_source: String::new(),
468+
};
469+
let prepared = prepare_mdx(
470+
"posts/hello",
471+
"/project/hello.mdx",
472+
source,
473+
&options,
474+
false,
475+
std::path::Path::new("/project"),
476+
&NativeMediaConfig::default(),
477+
)
478+
.unwrap();
479+
let module = finish_mdx(
480+
prepared,
481+
"/project/hello.mdx",
482+
source,
483+
&options,
484+
&json!({}),
485+
&json!({}),
486+
)
487+
.unwrap();
488+
489+
assert_eq!(module.matches("_components.br").count(), 1);
490+
}
491+
407492
#[test]
408493
fn rewrites_markdown_media_but_not_authored_jsx() {
409494
let root = std::env::temp_dir().join(format!("amamo-mdx-media-{}", std::process::id()));

native/src/hast.rs

Lines changed: 0 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,6 @@ enum HastWire {
5858
Text { value: String },
5959
}
6060

61-
pub fn apply_hard_breaks(tree: &mut Node) {
62-
walk(tree, false);
63-
}
64-
6561
pub fn decode_highlights(json: &str) -> Result<Vec<HighlightReplacement>, Vec<Diagnostic>> {
6662
let wires = serde_json::from_str::<Vec<HighlightWire>>(json).map_err(|error| {
6763
vec![Diagnostic::error(
@@ -448,96 +444,3 @@ pub(crate) fn normalize_path(path: &Path) -> PathBuf {
448444
}
449445
normalized
450446
}
451-
452-
fn walk(node: &mut Node, protected: bool) {
453-
let protected = protected
454-
|| matches!(node, Node::Element(element) if element.tag_name == "pre" || element.tag_name == "code");
455-
let Some(children) = node.children_mut() else {
456-
return;
457-
};
458-
459-
let old_children = std::mem::take(children);
460-
for mut child in old_children {
461-
if !protected
462-
&& let Node::Text(text) = &child
463-
&& text.value.contains('\n')
464-
{
465-
push_text_with_breaks(children, text);
466-
continue;
467-
}
468-
walk(&mut child, protected);
469-
children.push(child);
470-
}
471-
}
472-
473-
fn push_text_with_breaks(children: &mut Vec<Node>, text: &Text) {
474-
let parts = text.value.split('\n').collect::<Vec<_>>();
475-
for (index, part) in parts.iter().enumerate() {
476-
if !part.is_empty() {
477-
children.push(Node::Text(Text {
478-
value: (*part).into(),
479-
position: text.position.clone(),
480-
}));
481-
}
482-
if index + 1 < parts.len() {
483-
children.push(Node::Element(Element {
484-
tag_name: "br".into(),
485-
properties: vec![],
486-
children: vec![],
487-
position: None,
488-
}));
489-
children.push(Node::Text(Text {
490-
value: "\n".into(),
491-
position: None,
492-
}));
493-
}
494-
}
495-
}
496-
497-
#[cfg(test)]
498-
mod tests {
499-
use mdxjs::hast::{Element, Node, Text};
500-
501-
use super::apply_hard_breaks;
502-
503-
#[test]
504-
fn turns_soft_newlines_into_break_elements() {
505-
let mut tree = Node::Element(Element {
506-
tag_name: "p".into(),
507-
properties: vec![],
508-
children: vec![Node::Text(Text {
509-
value: "first\nsecond".into(),
510-
position: None,
511-
})],
512-
position: None,
513-
});
514-
515-
apply_hard_breaks(&mut tree);
516-
517-
let children = tree.children().unwrap();
518-
assert_eq!(children.len(), 4);
519-
assert!(matches!(&children[1], Node::Element(element) if element.tag_name == "br"));
520-
}
521-
522-
#[test]
523-
fn leaves_code_newlines_untouched() {
524-
let mut tree = Node::Element(Element {
525-
tag_name: "pre".into(),
526-
properties: vec![],
527-
children: vec![Node::Element(Element {
528-
tag_name: "code".into(),
529-
properties: vec![],
530-
children: vec![Node::Text(Text {
531-
value: "first\nsecond".into(),
532-
position: None,
533-
})],
534-
position: None,
535-
})],
536-
position: None,
537-
});
538-
539-
apply_hard_breaks(&mut tree);
540-
541-
assert_eq!(tree.to_string(), "first\nsecond");
542-
}
543-
}

src/__tests__/native.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ test('injects real Shiki HAST into the compiled module', async () => {
116116
try {
117117
const records = batch.finish(await renderer.highlight(batch.codeBlocks))
118118
assert.match(records[0]?.module ?? '', /--shiki-dark/)
119+
assert.match(records[0]?.module ?? '', /"backgroundColor": "#ffffff"/)
119120
assert.match(records[0]?.module ?? '', /style: \{/)
120121
assert.doesNotMatch(records[0]?.module ?? '', /style: "--shiki-dark/)
121122
assert.match(records[0]?.module ?? '', /language-ts/)

src/__tests__/shiki.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ test('uses official Shiki dual-theme HAST', async () => {
2828
])
2929

3030
assert.equal(result.length, 1)
31-
assert.match(JSON.stringify(result[0]?.hast), /--shiki-dark/)
31+
const hast = JSON.stringify(result[0]?.hast)
32+
assert.match(hast, /background-color:#f0efea/)
33+
assert.match(hast, /--shiki-dark/)
3234
} finally {
3335
await renderer.dispose()
3436
}

src/shiki.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,11 @@ export async function createShikiRenderer(
8080
const language = block.lang
8181
const hast = highlighter.codeToHast(block.code, {
8282
colorReplacements: config.colorReplacements,
83-
defaultColor: false,
8483
lang: languages[index] ?? 'text',
8584
meta: block.meta ? { __raw: block.meta } : undefined,
8685
themes: {
87-
dark: config.themes.dark,
8886
light: config.themes.light,
87+
dark: config.themes.dark,
8988
},
9089
transformers:
9190
language && !['text', 'plain', 'plaintext'].includes(language)

0 commit comments

Comments
 (0)