Visitar URL original
feat: nativeMagicString lastChar · rolldown/rolldown@fae065d · GitHub
Skip to content

Commit fae065d

Browse files
feat: nativeMagicString lastChar
1 parent 0975145 commit fae065d

5 files changed

Lines changed: 131 additions & 3 deletions

File tree

‎crates/rolldown_binding/src/types/binding_magic_string.rs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,18 @@ impl BindingMagicString<'_> {
257257
Self { inner: self.inner.clone(), char_to_byte_mapper: self.char_to_byte_mapper.clone() }
258258
}
259259

260+
/// Returns the last character of the generated string, or an empty string if empty.
261+
#[napi]
262+
pub fn last_char(&self) -> String {
263+
self.inner.last_char().map(|c| c.to_string()).unwrap_or_default()
264+
}
265+
266+
/// Returns the content after the last newline in the generated string.
267+
#[napi]
268+
pub fn last_line(&self) -> String {
269+
self.inner.last_line()
270+
}
271+
260272
/// Returns a clone with content outside the specified range removed.
261273
#[napi]
262274
#[must_use]

‎crates/string_wizard/src/magic_string/mod.rs‎

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,116 @@ impl<'text> MagicString<'text> {
100100
self.source.len() != self.len() || self.source.as_ref() != self.to_string()
101101
}
102102

103+
/// Returns the last character of the generated string, or `None` if empty.
104+
pub fn last_char(&self) -> Option<char> {
105+
// Check outro first (last in output order)
106+
if let Some(last_outro) = self.outro.back() {
107+
if let Some(c) = last_outro.chars().last() {
108+
return Some(c);
109+
}
110+
}
111+
112+
// Traverse chunks from last to first
113+
let mut chunk_idx = Some(self.last_chunk_idx);
114+
while let Some(idx) = chunk_idx {
115+
let chunk = &self.chunks[idx];
116+
117+
// Check chunk outro
118+
if let Some(last_outro) = chunk.outro.back() {
119+
if let Some(c) = last_outro.chars().last() {
120+
return Some(c);
121+
}
122+
}
123+
124+
// Check chunk content (edited or original)
125+
let content = chunk
126+
.edited_content
127+
.as_ref()
128+
.map(|s| s.as_ref())
129+
.unwrap_or_else(|| chunk.span.text(&self.source));
130+
if let Some(c) = content.chars().last() {
131+
return Some(c);
132+
}
133+
134+
// Check chunk intro
135+
if let Some(last_intro) = chunk.intro.back() {
136+
if let Some(c) = last_intro.chars().last() {
137+
return Some(c);
138+
}
139+
}
140+
141+
chunk_idx = chunk.prev;
142+
}
143+
144+
// Check intro last (first in output order, but we're going backwards)
145+
if let Some(last_intro) = self.intro.back() {
146+
if let Some(c) = last_intro.chars().last() {
147+
return Some(c);
148+
}
149+
}
150+
151+
None
152+
}
153+
154+
/// Returns the content after the last newline in the generated string.
155+
pub fn last_line(&self) -> String {
156+
// Check outro first (last in output order)
157+
for outro_part in self.outro.iter().rev() {
158+
if let Some(line_index) = memchr::memrchr(b'\n', outro_part.as_bytes()) {
159+
return outro_part[line_index + 1..].to_string();
160+
}
161+
}
162+
163+
let mut line_str = self.outro.iter().map(|s| s.as_ref()).collect::<String>();
164+
165+
// Traverse chunks from last to first
166+
let mut chunk_idx = Some(self.last_chunk_idx);
167+
while let Some(idx) = chunk_idx {
168+
let chunk = &self.chunks[idx];
169+
170+
// Check chunk outro
171+
for outro_part in chunk.outro.iter().rev() {
172+
if let Some(line_index) = memchr::memrchr(b'\n', outro_part.as_bytes()) {
173+
return outro_part[line_index + 1..].to_string() + &line_str;
174+
}
175+
}
176+
let chunk_outro: String = chunk.outro.iter().map(|s| s.as_ref()).collect();
177+
line_str = chunk_outro + &line_str;
178+
179+
// Check chunk content (edited or original)
180+
let content = chunk
181+
.edited_content
182+
.as_ref()
183+
.map(|s| s.as_ref())
184+
.unwrap_or_else(|| chunk.span.text(&self.source));
185+
if let Some(line_index) = memchr::memrchr(b'\n', content.as_bytes()) {
186+
return content[line_index + 1..].to_string() + &line_str;
187+
}
188+
line_str = content.to_string() + &line_str;
189+
190+
// Check chunk intro
191+
for intro_part in chunk.intro.iter().rev() {
192+
if let Some(line_index) = memchr::memrchr(b'\n', intro_part.as_bytes()) {
193+
return intro_part[line_index + 1..].to_string() + &line_str;
194+
}
195+
}
196+
let chunk_intro: String = chunk.intro.iter().map(|s| s.as_ref()).collect();
197+
line_str = chunk_intro + &line_str;
198+
199+
chunk_idx = chunk.prev;
200+
}
201+
202+
// Check intro last (first in output order, but we're going backwards)
203+
for intro_part in self.intro.iter().rev() {
204+
if let Some(line_index) = memchr::memrchr(b'\n', intro_part.as_bytes()) {
205+
return intro_part[line_index + 1..].to_string() + &line_str;
206+
}
207+
}
208+
209+
let intro_str: String = self.intro.iter().map(|s| s.as_ref()).collect();
210+
intro_str + &line_str
211+
}
212+
103213
fn prepend_intro(&mut self, content: impl Into<CowStr<'text>>) {
104214
self.intro.push_front(content.into());
105215
}

‎packages/rolldown/src/binding.d.cts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1461,6 +1461,10 @@ export declare class BindingMagicString {
14611461
insert(index: number, content: string): void
14621462
/** Returns a clone of the MagicString instance. */
14631463
clone(): BindingMagicString
1464+
/** Returns the last character of the generated string, or an empty string if empty. */
1465+
lastChar(): string
1466+
/** Returns the content after the last newline in the generated string. */
1467+
lastLine(): string
14641468
/** Returns a clone with content outside the specified range removed. */
14651469
snip(start: number, end: number): BindingMagicString
14661470
/**

‎packages/rolldown/tests/magic-string/MagicString.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1762,7 +1762,7 @@ describe('MagicString', () => {
17621762
});
17631763
});
17641764

1765-
describe.skip('lastLine', () => {
1765+
describe('lastLine', () => {
17661766
it('should support lastLine', () => {
17671767
const s = new MagicString(' abcde\nfghijkl ');
17681768

‎packages/rolldown/tests/magic-string/download-tests.mjs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
* - insert(index: number, content: string): throws Error (deprecated)
3535
* - clone(): BindingMagicString
3636
* - snip(start: number, end: number): BindingMagicString
37+
* - lastChar(): string
38+
* - lastLine(): string
3739
*
3840
* NOT supported (will be skipped):
3941
* - constructor options (filename, ignoreList, indentExclusionRanges)
@@ -60,11 +62,11 @@ const SKIP_DESCRIBE_BLOCKS = [
6062
'generateDecodedMap',
6163
'generateMap',
6264
'getIndentString', // not supported
63-
'lastChar',
64-
'lastLine',
6565
'original',
6666
'reset',
6767
// Note: 'snip' is now supported
68+
// Note: 'lastChar' is now supported
69+
// Note: 'lastLine' is now supported
6870
// Note: 'options' is now partially supported (filename works, ignoreList doesn't)
6971
// Note: 'insert' is now supported (throws deprecated error as expected)
7072
// Note: 'slice' is now supported

0 commit comments

Comments
 (0)