diff --git a/benchmark/bench.ts b/benchmark/bench.ts index 2bcd066..a2959b9 100644 --- a/benchmark/bench.ts +++ b/benchmark/bench.ts @@ -160,4 +160,32 @@ b.suite( }), b.cycle(), b.complete(), -) \ No newline at end of file +) + +b.suite( + 'clone', + b.add('MagicString', () => { + const m = new MagicString(`export const foo = 'bar'`) + m.clone() + }), + b.add('MagicStringRust', () => { + const m = new MagicStringRust(`export const foo = 'bar'`) + m.clone() + }), + b.cycle(), + b.complete(), +) + +b.suite( + 'snip', + b.add('MagicString', () => { + const m = new MagicString(`export const foo = 'bar'`) + m.snip(3, 9) + }), + b.add('MagicStringRust', () => { + const m = new MagicStringRust(`export const foo = 'bar'`) + m.snip(3, 9) + }), + b.cycle(), + b.complete(), +) diff --git a/core/src/magic_string.rs b/core/src/magic_string.rs index 325afde..8def03c 100644 --- a/core/src/magic_string.rs +++ b/core/src/magic_string.rs @@ -1,4 +1,11 @@ -use std::{cell::RefCell, collections::HashMap, rc::Rc, string::ToString}; +use std::{ + borrow::{Borrow, BorrowMut}, + cell::RefCell, + collections::HashMap, + ops::Deref, + rc::Rc, + string::ToString, +}; use crate::utils::{normalize_index, trim}; @@ -60,7 +67,7 @@ pub struct DecodedMap { #[derive(Debug, Clone)] pub struct MagicString { - original_str: String, + pub original: String, original_str_locator: Locator, intro: String, @@ -91,7 +98,7 @@ impl MagicString { let original_chunk = Rc::new(RefCell::new(Chunk::new(0u32, str.len() as u32, str))); MagicString { - original_str: String::from(str), + original: String::from(str), intro: String::default(), outro: String::default(), @@ -156,7 +163,8 @@ impl MagicString { self._split_at_index(index)?; if let Some(chunk) = self.chunk_by_end.get(&index) { - chunk.borrow_mut().prepend_outro(str); + // chunk.borrow_mut().prepend_outro(str); + chunk.deref().borrow_mut().prepend_outro(str); } else { self.intro = format!("{}{}", str, self.intro) }; @@ -171,7 +179,7 @@ impl MagicString { self._split_at_index(index)?; if let Some(chunk) = self.chunk_by_start.get(&index) { - chunk.borrow_mut().prepend_intro(str); + chunk.deref().borrow_mut().prepend_intro(str); } else { self.outro = format!("{}{}", str, self.outro) }; @@ -187,7 +195,7 @@ impl MagicString { self._split_at_index(index)?; if let Some(chunk) = self.chunk_by_end.get(&index) { - chunk.borrow_mut().append_outro(str); + chunk.deref().borrow_mut().append_outro(str); } else { self.intro = format!("{}{}", self.intro, str); }; @@ -203,7 +211,7 @@ impl MagicString { self._split_at_index(index)?; if let Some(chunk) = self.chunk_by_start.get(&index) { - chunk.borrow_mut().append_intro(str); + chunk.deref().borrow_mut().append_intro(str); } else { self.append(str)?; }; @@ -211,6 +219,65 @@ impl MagicString { Ok(self) } + pub fn clone(&self) -> Result { + let mut cloned = MagicString::new(self.original.borrow()); + + let mut original_chunk = Rc::clone(&self.first_chunk); + let mut cloned_chunk = Rc::new(RefCell::new(original_chunk.deref().borrow().clone())); + cloned.last_searched_chunk = Rc::clone(&cloned_chunk); + cloned.first_chunk = Rc::clone(&cloned_chunk); + + while let Some(c) = Some(Rc::clone(&original_chunk)) { + cloned + .chunk_by_start + .remove(&cloned_chunk.deref().borrow().start); + cloned + .chunk_by_end + .remove(&cloned_chunk.deref().borrow().end); + + cloned.chunk_by_start.insert( + cloned_chunk.deref().borrow().start, + Rc::clone(&cloned_chunk), + ); + cloned + .chunk_by_end + .insert(cloned_chunk.deref().borrow().end, Rc::clone(&cloned_chunk)); + // cloned.chunk_by_start[&cloned_chunk.deref().borrow().start] = Rc::clone(&cloned_chunk) ; + // cloned.chunk_by_end[&cloned_chunk.deref().borrow().end] = Rc::clone(&cloned_chunk); + + let original_chunk_clone = original_chunk.clone(); + let next_original_chunk = &original_chunk_clone.deref().borrow().next; + let next_cloned_chunk = match next_original_chunk { + None => None, + Some(chunk) => Some(Rc::new(RefCell::new(chunk.deref().borrow().clone()))) + }; + + match next_cloned_chunk { + None => { + break; + } + Some(chunk) => { + cloned_chunk.deref().borrow_mut().next = Some(Rc::clone(&chunk)); + chunk.deref().borrow_mut().prev = Some(Rc::clone(&cloned_chunk)); + cloned_chunk = chunk; + original_chunk = Rc::clone(next_original_chunk.as_ref().unwrap()); + } + } + } + + cloned.last_chunk = cloned_chunk; + // TODO: + /* if (this.indentExclusionRanges) { + cloned.indentExclusionRanges = this.indentExclusionRanges.slice(); + } + + cloned.sourcemapLocations = new BitSet(this.sourcemapLocations); */ + cloned.intro = self.intro.clone(); + cloned.outro = self.outro.clone(); + + Ok(cloned) + } + /// ## Overwrite /// /// Replaces the characters from start to end with content. Returns `self`. @@ -236,13 +303,15 @@ impl MagicString { content: &str, options: OverwriteOptions, ) -> Result<&mut Self> { + let content_only = options.content_only; - let start = normalize_index(self.original_str.as_str(), start)?; - let end = normalize_index(self.original_str.as_str(), end)?; + let start = normalize_index(self.original.as_str(), start)?; + let end = normalize_index(self.original.as_str(), end)?; let start = start as u32; let end = end as u32; + if start == end { return Err(Error::new_with_reason( MagicStringErrorType::MagicStringOutOfRangeError, @@ -257,21 +326,25 @@ impl MagicString { )); } + self._split_at_index(start)?; self._split_at_index(end)?; + + let start_chunk: Option>> = self.chunk_by_start.get(&start).map(Rc::clone); let end_chunk: Option>> = self.chunk_by_end.get(&end).map(Rc::clone); if let Some(start_chunk) = start_chunk { + // Note: This original implementation looks a little bit weird to me. // It should check whether the latter chunks had been edited(not only for content-wise, but also for intro and outro) or not, // then we could return the Error. But for now, It's been doing just fine. - if start_chunk.borrow().end < end - && (start_chunk.borrow().next + if start_chunk.deref().borrow().end < end + && (start_chunk.deref().borrow().next != self .chunk_by_start - .get(&start_chunk.borrow().end) + .get(&start_chunk.deref().borrow().end) .map(Rc::clone)) { return Err(Error::new_with_reason( @@ -282,28 +355,29 @@ impl MagicString { Chunk::try_each_next(Rc::clone(&start_chunk), |chunk| { if start_chunk == chunk { - start_chunk.borrow_mut().content = content.to_owned(); + start_chunk.deref().borrow_mut().content = content.to_owned(); if !content_only { - start_chunk.borrow_mut().intro = String::default(); - start_chunk.borrow_mut().outro = String::default(); + start_chunk.deref().borrow_mut().intro = String::default(); + start_chunk.deref().borrow_mut().outro = String::default(); } return Ok(false); } if end_chunk.is_some() - && chunk.borrow().start + && chunk.deref().borrow().start >= (end_chunk.as_ref().map(Rc::clone).unwrap() as Rc>) + .deref() .borrow() .end { return Ok(true); } - chunk.borrow_mut().content = String::default(); + chunk.deref().borrow_mut().content = String::default(); if !content_only { - chunk.borrow_mut().intro = String::default(); - chunk.borrow_mut().outro = String::default(); + chunk.deref().borrow_mut().intro = String::default(); + chunk.deref().borrow_mut().outro = String::default(); } Ok(false) @@ -379,11 +453,11 @@ impl MagicString { Chunk::try_each_next(Rc::clone(&self.first_chunk), |chunk| { self.last_searched_chunk = Rc::clone(&chunk); - if let Err(e) = chunk.borrow_mut().trim_start_regexp(pattern) { + if let Err(e) = chunk.deref().borrow_mut().trim_start_regexp(pattern) { return Err(e); } - Ok(!chunk.borrow().to_string().is_empty()) + Ok(!chunk.deref().borrow().to_string().is_empty()) })?; if error != Error::default() { @@ -391,7 +465,13 @@ impl MagicString { } if self.last_searched_chunk == self.last_chunk - && self.last_chunk.borrow().content.to_string().is_empty() + && self + .last_chunk + .deref() + .borrow() + .content + .to_string() + .is_empty() { self.outro = trim::trim_start_regexp(self.outro.as_str(), pattern)?.to_owned() } @@ -434,15 +514,21 @@ impl MagicString { Chunk::try_each_prev(Rc::clone(&self.last_chunk), |chunk| { self.last_searched_chunk = Rc::clone(&chunk); - if let Err(e) = chunk.borrow_mut().trim_end_regexp(pattern) { + if let Err(e) = chunk.deref().borrow_mut().trim_end_regexp(pattern) { return Err(e); } - Ok(!chunk.borrow().to_string().is_empty()) + Ok(!chunk.deref().borrow().to_string().is_empty()) })?; if self.last_searched_chunk == self.first_chunk - && self.first_chunk.borrow().content.to_string().is_empty() + && self + .first_chunk + .deref() + .borrow() + .content + .to_string() + .is_empty() { self.intro = trim::trim_end_regexp(self.intro.as_str(), pattern)?.to_owned() } @@ -488,8 +574,8 @@ impl MagicString { /// /// ``` pub fn remove(&mut self, start: i64, end: i64) -> Result<&mut Self> { - let start = normalize_index(self.original_str.as_str(), start)?; - let end = normalize_index(self.original_str.as_str(), end)?; + let start = normalize_index(self.original.as_str(), start)?; + let end = normalize_index(self.original.as_str(), end)?; let start = start as u32; let end = end as u32; @@ -514,9 +600,9 @@ impl MagicString { if start_chunk.is_some() { Chunk::try_each_next(start_chunk.map(Rc::clone).unwrap(), |chunk| { - chunk.borrow_mut().content = String::default(); - chunk.borrow_mut().intro = String::default(); - chunk.borrow_mut().outro = String::default(); + chunk.deref().borrow_mut().content = String::default(); + chunk.deref().borrow_mut().intro = String::default(); + chunk.deref().borrow_mut().outro = String::default(); Ok(chunk == Rc::clone(end_chunk.unwrap())) })?; @@ -525,6 +611,105 @@ impl MagicString { Ok(self) } + /// ## Slice + /// Get a slice of the modified string. + /// Example: + /// ``` + /// use magic_string::MagicString; + /// let mut s = MagicString::new("abcdefghijkl"); + /// + /// + /// ``` + /// + pub fn slice(&mut self, start: i64, end: i64) -> Result { + let start = normalize_index(self.original.as_str(), start)?; + let end = normalize_index(self.original.as_str(), end)?; + + let start = start as u32; + let end = end as u32; + + if start > end { + return Err(Error::new_with_reason( + MagicStringErrorType::MagicStringOutOfRangeError, + "Start must be greater than end.", + )); + } + + let mut result = String::new(); + let mut chunk = Some(Rc::clone(&self.first_chunk)); + while let Some(c) = chunk.clone() { + if c.deref().borrow().start > start || c.deref().borrow().end <= start { + chunk = c.deref().borrow().clone().next; + } else { + break; + } + } + if let Some(c) = chunk.clone() { + if c.deref().borrow().is_content_edited() && c.deref().borrow().start != start { + return Err(Error::new_with_reason( + MagicStringErrorType::MagicStringUnknownError, + "Cannot move a selection inside itself", + )); + } + } + let start_chunk = chunk.clone().unwrap(); + Chunk::try_each_next(Rc::clone(&chunk.unwrap()), |chunk| { + let str: &str; + + if chunk.deref().borrow().intro.len() != 0 + && (start_chunk != chunk || chunk.deref().borrow().start == start) + { + result.push_str(chunk.deref().borrow().intro.as_str()); + }; + + let contain_end = chunk.deref().borrow().end >= end; + + let slice_start = if chunk.deref().borrow().start < start { + start - chunk.deref().borrow().start + } else { + 0 + }; + let slice_end = if contain_end { + chunk.deref().borrow().content.len() as u32 + end - chunk.deref().borrow().end + } else { + chunk.deref().borrow().content.len() as u32 + }; + + let chunk_str = chunk.deref().borrow().content.clone(); + + if contain_end + && chunk.deref().borrow().is_content_edited() + && chunk.deref().borrow().end != end + { + return Err(Error::new_with_reason( + MagicStringErrorType::MagicStringUnknownError, + "Cannot use replaced character ${end} as slice end anchor.", + )); + } + + str = &chunk_str.as_str()[slice_start as usize..slice_end as usize]; + result.push_str(str); + if chunk.deref().borrow().outro.len() != 0 + && (!contain_end || chunk.deref().borrow().end == end) + { + result.push_str(chunk.deref().borrow().outro.as_str()) + } + + Ok(chunk.deref().borrow().end >= end) + })?; + + Ok(result) + } + + pub fn snip(&mut self, start: i64, end: i64) -> Result { + let mut clone = self.clone()?; + let length = clone.original.len(); + clone.remove(0, start)?; + clone.remove(end, length as i64)?; + + Ok(clone) + } + /// ## Is empty /// /// Returns `true` if the resulting source is empty (disregarding white space). @@ -582,7 +767,7 @@ impl MagicString { map.advance(self.intro.as_str()); Chunk::try_each_next(Rc::clone(&self.first_chunk), |chunk| { - let loc = locator.locate(chunk.borrow().start); + let loc = locator.locate(chunk.deref().borrow().start); map.add_chunk(Rc::clone(&chunk), loc); Ok(false) })?; @@ -597,7 +782,7 @@ impl MagicString { names: Vec::default(), sources_content: { if options.include_content { - vec![Some(self.original_str.to_owned())] + vec![Some(self.original.to_owned())] } else { Default::default() } @@ -639,21 +824,30 @@ impl MagicString { let chunk = Rc::clone(&self.last_searched_chunk); - let search_forward = index > chunk.borrow().start; + let search_forward = index > chunk.deref().borrow().start; let mut curr = Some(chunk); while let Some(c) = curr { - if c.borrow().contains(index) { + + if c.deref().borrow().contains(index) { + self._split_chunk_at_index(c, index)?; return Ok(()); } else { + curr = { if search_forward { - self.chunk_by_start.get(&c.borrow().end).map(Rc::clone) + self + .chunk_by_start + .get(&c.deref().borrow().end) + .map(Rc::clone) } else { - self.chunk_by_end.get(&c.borrow().start).map(Rc::clone) + self + .chunk_by_end + .get(&c.deref().borrow().start) + .map(Rc::clone) } - } + }; } } @@ -662,14 +856,14 @@ impl MagicString { fn _split_chunk_at_index(&mut self, chunk: Rc>, index: u32) -> Result { // Zero-length edited chunks can be split into different chunks, cause split chunks are the same. - if chunk.borrow().is_content_edited() && !chunk.borrow().content.is_empty() { + if chunk.deref().borrow().is_content_edited() && !chunk.deref().borrow().content.is_empty() { return Err(Error::new( MagicStringErrorType::MagicStringDoubleSplitError, )); } let new_chunk = Chunk::split(Rc::clone(&chunk), index); - let new_chunk_original = new_chunk.borrow(); + let new_chunk_original = new_chunk.deref().borrow(); self.chunk_by_end.insert(index, Rc::clone(&chunk)); self.chunk_by_start.insert(index, Rc::clone(&new_chunk)); @@ -704,7 +898,7 @@ impl ToString for MagicString { let mut str = self.intro.to_owned(); Chunk::try_each_next(Rc::clone(&self.first_chunk), |chunk| { - str = format!("{}{}", str, chunk.borrow().to_string()); + str = format!("{}{}", str, chunk.deref().borrow().to_string()); Ok(false) }) .unwrap(); diff --git a/core/tests/clone.rs b/core/tests/clone.rs new file mode 100644 index 0000000..c3d6457 --- /dev/null +++ b/core/tests/clone.rs @@ -0,0 +1,31 @@ +#[cfg(test)] +mod remove { + use magic_string::{MagicString, OverwriteOptions, Result}; + + #[test] + fn should_clone_a_magic_string() -> Result { + + let mut s = MagicString::new("abcdefghijkl"); + s.overwrite(3, 9, "XYZ", OverwriteOptions::default())?; + let mut c = s.clone().unwrap(); + c.overwrite(3, 9, "XYZB", OverwriteOptions::default())?; + + assert_eq!(s.to_string(), "abcXYZjkl"); + assert_eq!(c.to_string(), "abcXYZBjkl"); + + Ok(()) + } + + #[test] + fn should_clone_intro_and_outro() -> Result { + + let mut s = MagicString::new("defghi"); + s.prepend("abc")?; + s.append("jkl")?; + let c = s.clone().unwrap(); + + assert_eq!(s.to_string(), c.to_string()); + + Ok(()) + } +} diff --git a/core/tests/slice.rs b/core/tests/slice.rs new file mode 100644 index 0000000..5fbd644 --- /dev/null +++ b/core/tests/slice.rs @@ -0,0 +1,104 @@ +#[cfg(test)] + +mod slice { + use magic_string::{MagicString, OverwriteOptions, Result}; + + #[test] + fn should_return_the_generated_content_between_the_specified_original_characters() -> Result { + let mut s = MagicString::new("abcdefghijkl"); + + assert_eq!(s.slice(3, 9)?, "defghi"); + s.overwrite(4, 8, "XX", OverwriteOptions::default())?; + assert_eq!(s.slice(3, 9)?, "dXXi"); + s.overwrite(2, 10, "ZZ", OverwriteOptions::default())?; + assert_eq!(s.slice(1, 11)?, "bZZk"); + assert_eq!(s.slice(2, 10)?, "ZZ"); + + Ok(()) + } + + // #[test] + // fn defaults_end_to_the_original_string_length() -> Result { + // let mut s = MagicString::new("abcdefghijkl"); + // assert_eq!(s.slice(3)?, "defghijkl"); + // } + + #[test] + fn allow_negative_numbers_as_params() -> Result { + let mut s = MagicString::new("abcdefghijkl"); + + assert_eq!(s.slice(0, -3)?, "abcdefghi"); + // assert_eq!(s.slice(-3)?, "jkl"); + Ok(()) + } + #[test] + fn includes_inserted_characters_respecting_insertion_direction() -> Result { + let mut s = MagicString::new("abefij"); + + s.prepend_right(2, "cd")?; + s.append_left(4, "gh")?; + + // assert_eq!(s.slice(), "abcdefghij"); + assert_eq!(s.slice(1, 5)?, "bcdefghi"); + assert_eq!(s.slice(2, 4)?, "cdefgh"); + assert_eq!(s.slice(3, 4)?, "fgh"); + assert_eq!(s.slice(0, 2)?, "ab"); + assert_eq!(s.slice(0, 3)?, "abcde"); + assert_eq!(s.slice(4, 6)?, "ij"); + assert_eq!(s.slice(3, 6)?, "fghij"); + Ok(()) + } + + // wating for move to be implemented + // #[test] + // fn supports_characters_moved_outward() -> Result { + // let mut s = MagicString::new("abcdEFghIJklmn"); + + // s._move(4, 6, 2)?; + // s._move(8, 10, 12)?; + // assert_eq!(s.to_string(), "abEFcdghklIJmn"); + + // assert_eq!(s.slice(1, -1)?, "bEFcdghklIJm"); + // assert_eq!(s.slice(2, -2)?, "cdghkl"); + // assert_eq!(s.slice(3, -3)?, "dghk"); + // assert_eq!(s.slice(4, -4)?, "EFcdghklIJ"); + // assert_eq!(s.slice(5, -5)?, "FcdghklI"); + // assert_eq!(s.slice(6, -6)?, "gh"); + // Ok(()) + // } + + // #[test] + // fn supports_characters_moved_inward() -> Result { + // let mut s = MagicString::new("abCDefghijKLmn"); + // s._move(2, 4, 6)?; + // s._move(10, 12, 8)?; + // assert_eq!(s.to_string(), "abefCDghKLijmn"); + + // assert_eq!(s.slice(1, -1)?, "befCDghKLijm"); + // assert_eq!(s.slice(2, -2)?, "CDghKL"); + // assert_eq!(s.slice(3, -3)?, "DghK"); + // assert_eq!(s.slice(4, -4)?, "efCDghKLij"); + // assert_eq!(s.slice(5, -5)?, "fCDghKLi"); + // assert_eq!(s.slice(6, -6)?, "gh"); + // Ok(()) + // } + + // #[test] + // fn supports_characters_moved_inward() -> Result { + // let mut s = MagicString::new("abCDefghIJkl"); + // // s._move(2, 4, 8)?; + // // s._move(8, 10, 4)?; + // assert_eq!(s.to_string(), "abIJefghCDkl"); + + // assert_eq!(s.slice(1, -1)?, "bIJefghCDk"); + // assert_eq!(s.slice(2, -2)?, ""); + // assert_eq!(s.slice(3, -3)?, ""); + // assert_eq!(s.slice(-3, 3)?, "JefghC"); + // assert_eq!(s.slice(4, -4)?, "efgh"); + // assert_eq!(s.slice(0, 3)?, "abIJefghC"); + // // assert_eq!(s.slice(3)?, "Dkl"); + // assert_eq!(s.slice(0, -3)?, "abI"); + // // assert_eq!(s.slice(-3)?, "JefghCDkl"); + // Ok(()) + // } +} diff --git a/core/tests/snip.rs b/core/tests/snip.rs new file mode 100644 index 0000000..ee7dc95 --- /dev/null +++ b/core/tests/snip.rs @@ -0,0 +1,50 @@ +#[cfg(test)] +mod remove { + use magic_string::{MagicString, OverwriteOptions, Result}; + + #[test] + fn should_return_a_clone_with_content_outside_start_and_end_removed() -> Result { + + let mut s = MagicString::new("abcdefghijkl"); + s.overwrite(6, 9, "GHI", OverwriteOptions::default())?; + let snippet = s.snip(3, 9)?; + + assert_eq!(snippet.to_string(), "defGHI"); + + Ok(()) + } + + #[test] + fn should_snip_from_the_start() -> Result { + + let mut s = MagicString::new("abcdefghijkl"); + let snippet = s.snip(0, 6)?; + + assert_eq!(snippet.to_string(), "abcdef"); + + Ok(()) + } + + #[test] + fn should_snip_from_the_end() -> Result { + + let mut s = MagicString::new("abcdefghijkl"); + let snippet = s.snip(6, 12)?; + + assert_eq!(snippet.to_string(), "ghijkl"); + + Ok(()) + } + + #[test] + fn should_respect_original_indices() -> Result { + + let mut s = MagicString::new("abcdefghijkl"); + let mut snippet = s.snip(3, 9)?; + + snippet.overwrite(6, 9, "GHI", OverwriteOptions::default())?; + assert_eq!(snippet.to_string(), "defGHI"); + + Ok(()) + } +} diff --git a/node/index.d.ts b/node/index.d.ts index 0c37f8c..6e29563 100644 --- a/node/index.d.ts +++ b/node/index.d.ts @@ -11,18 +11,18 @@ export class ExternalObject { } /** Only for .d.ts type generation */ export interface DecodedMap { - file?: string | undefined | null + file?: string sources: Array - sourceRoot?: string | undefined | null + sourceRoot?: string sourcesContent: Array names: Array mappings: Array>> } /** Only for .d.ts generation */ export interface GenerateDecodedMapOptions { - file?: string | undefined | null - sourceRoot?: string | undefined | null - source?: string | undefined | null + file?: string + sourceRoot?: string + source?: string includeContent: boolean hires: boolean } @@ -32,12 +32,15 @@ export interface OverwriteOptions { } export class MagicString { constructor(originalStr: string) + get original(): string append(input: string): this prepend(input: string): this appendLeft(index: number, input: string): this appendRight(index: number, input: string): this prependLeft(index: number, input: string): this prependRight(index: number, input: string): this + clone(): MagicString + snip(start: number, end: number): MagicString overwrite( start: number, end: number, @@ -49,6 +52,7 @@ export class MagicString { trimEnd(pattern?: string | undefined | null): this trimLines(): this remove(start: number, end: number): this + slice(start: number, end: number): string isEmpty(): boolean generateMap(options?: Partial): { toString: () => string diff --git a/node/index.js b/node/index.js index d9481a5..940d7ff 100644 --- a/node/index.js +++ b/node/index.js @@ -1,55 +1,51 @@ const { MagicString: MagicStringNative } = require('./binding') -module.exports.MagicString = class MagicString extends MagicStringNative { - overwrite(start, end, content, options) { - options = { - contentOnly: false, - ...options - } - return super.overwrite(start, end, content, options) - } - generateMap(options) { - options = { - file: null, - source: null, - sourceRoot: null, - includeContent: false, - hires: false, - ...options, - } - - const toString = () => super.toSourcemapString(options) - const toUrl = () => super.toSourcemapUrl(options) - const toMap = () => JSON.parse(toString(options)) - - return { - toString, - toUrl, - toMap, - } +const nativeOverwrite = MagicStringNative.prototype.overwrite +MagicStringNative.prototype.overwrite = function overwrite(start, end, content, options) { + options = { + contentOnly: false, + ...options } - generateDecodedMap(options) { - options = { - file: null, - source: null, - sourceRoot: null, - includeContent: false, - hires: false, - ...options, - } - - return JSON.parse(super.generateDecodedMap(options)) + nativeOverwrite.call(this, start, end, content, options) + return this +} + +MagicStringNative.prototype.generateMap = function generateMap(options) { + options = { + file: null, + source: null, + sourceRoot: null, + includeContent: false, + hires: false, + ...options, } - toSourcemapString() { - throw new Error( - '[magic-string] This is an internal API, you may refer to `generateMap`', - ) + + const toString = () => this.toSourcemapString(options) + const toUrl = () => this.toSourcemapUrl(options) + const toMap = () => JSON.parse(toString(options)) + + return { + toString, + toUrl, + toMap, } - toSourcemapUrl() { - throw new Error( - '[magic-string] This is an internal API, you may refer to `generateMap`', - ) +} + +const nativeGenerateDecodedMap = MagicStringNative.prototype.generateDecodedMap +MagicStringNative.prototype.generateDecodedMap = function generateDecodedMap(options) { + options = { + file: null, + source: null, + sourceRoot: null, + includeContent: false, + hires: false, + ...options, } + + return JSON.parse(nativeGenerateDecodedMap.call(this, options)) +} + +module.exports.MagicString = class MagicString extends MagicStringNative { } Object.assign(exports, '__esModule', { diff --git a/node/src/lib.rs b/node/src/lib.rs index 224f16d..dcaa5b3 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -21,6 +21,11 @@ impl MagicString { MagicString(magic_string::MagicString::new(original_str.as_str())) } + #[napi(getter)] + pub fn original(&self) -> Result { + Ok(self.0.original.clone()) + } + #[napi] pub fn append(&mut self, input: String) -> Result<&Self> { self.0.append(input.as_str())?; @@ -57,6 +62,16 @@ impl MagicString { Ok(self) } + #[napi] + pub fn clone(&mut self) -> Result { + Ok(MagicString(self.0.clone()?)) + } + + #[napi] + pub fn snip(&mut self, start: i64, end: i64) -> Result { + Ok(MagicString(self.0.snip(start, end)?)) + } + #[napi(ts_args_type = r" start: number, end: number, @@ -104,6 +119,11 @@ impl MagicString { Ok(self) } + #[napi] + pub fn slice(&mut self, start: i64, end: i64) -> Result { + Ok(self.0.slice(start, end)?) + } + #[napi] pub fn is_empty(&self) -> Result { Ok(self.0.is_empty()) diff --git a/node/tests/MagicString.spec.ts b/node/tests/MagicString.spec.ts index 989d99c..5399330 100644 --- a/node/tests/MagicString.spec.ts +++ b/node/tests/MagicString.spec.ts @@ -110,89 +110,89 @@ describe('MagicString', () => { }) }) - // describe('clone', () => { - // it('should clone a magic string', () => { - // const s = new MagicString('abcdefghijkl') + describe('clone', () => { + it('should clone a magic string', () => { + const s = new MagicString('abcdefghijkl') - // s.overwrite(3, 9, 'XYZ') - // const c = s.clone() + s.overwrite(3, 9, 'XYZ') + const c = s.clone() - // assert.notEqual(s, c) - // assert.equal(c.toString(), 'abcXYZjkl') - // }) + assert.notEqual(s, c) + assert.equal(c.toString(), 'abcXYZjkl') + }) - // it('should clone filename info', () => { - // const s = new MagicString('abcdefghijkl', { filename: 'foo.js' }) - // const c = s.clone() + /* it('should clone filename info', () => { + const s = new MagicString('abcdefghijkl', { filename: 'foo.js' }) + const c = s.clone() - // assert.equal(c.filename, 'foo.js') - // }) + assert.equal(c.filename, 'foo.js') + }) - // it('should clone indentExclusionRanges', () => { - // const array = [3, 6] - // const source = new MagicString('abcdefghijkl', { - // filename: 'foo.js', - // indentExclusionRanges: array, - // }) + it('should clone indentExclusionRanges', () => { + const array = [3, 6] + const source = new MagicString('abcdefghijkl', { + filename: 'foo.js', + indentExclusionRanges: array, + }) - // const clone = source.clone() + const clone = source.clone() - // assert.notStrictEqual( - // source.indentExclusionRanges, - // clone.indentExclusionRanges, - // ) - // assert.deepEqual( - // source.indentExclusionRanges, - // clone.indentExclusionRanges, - // ) - // }) + assert.notStrictEqual( + source.indentExclusionRanges, + clone.indentExclusionRanges, + ) + assert.deepEqual( + source.indentExclusionRanges, + clone.indentExclusionRanges, + ) + }) - // it('should clone complex indentExclusionRanges', () => { - // const array = [ - // [3, 6], - // [7, 9], - // ] - // const source = new MagicString('abcdefghijkl', { - // filename: 'foo.js', - // indentExclusionRanges: array, - // }) + it('should clone complex indentExclusionRanges', () => { + const array = [ + [3, 6], + [7, 9], + ] + const source = new MagicString('abcdefghijkl', { + filename: 'foo.js', + indentExclusionRanges: array, + }) - // const clone = source.clone() + const clone = source.clone() - // assert.notStrictEqual( - // source.indentExclusionRanges, - // clone.indentExclusionRanges, - // ) - // assert.deepEqual( - // source.indentExclusionRanges, - // clone.indentExclusionRanges, - // ) - // }) + assert.notStrictEqual( + source.indentExclusionRanges, + clone.indentExclusionRanges, + ) + assert.deepEqual( + source.indentExclusionRanges, + clone.indentExclusionRanges, + ) + }) - // it('should clone sourcemapLocations', () => { - // const source = new MagicString('abcdefghijkl', { - // filename: 'foo.js', - // }) + it('should clone sourcemapLocations', () => { + const source = new MagicString('abcdefghijkl', { + filename: 'foo.js', + }) - // source.addSourcemapLocation(3) + source.addSourcemapLocation(3) - // const clone = source.clone() + const clone = source.clone() - // assert.notStrictEqual(source.sourcemapLocations, clone.sourcemapLocations) - // assert.deepEqual(source.sourcemapLocations, clone.sourcemapLocations) - // }) + assert.notStrictEqual(source.sourcemapLocations, clone.sourcemapLocations) + assert.deepEqual(source.sourcemapLocations, clone.sourcemapLocations) + }) */ - // it('should clone intro and outro', () => { - // const source = new MagicString('defghi') + it('should clone intro and outro', () => { + const source = new MagicString('defghi') - // source.prepend('abc') - // source.append('jkl') + source.prepend('abc') + source.append('jkl') - // const clone = source.clone() + const clone = source.clone() - // assert.equal(source.toString(), clone.toString()) - // }) - // }) + assert.equal(source.toString(), clone.toString()) + }) + }) describe('generateMap', () => { it('should generate a sourcemap', () => { @@ -1094,152 +1094,153 @@ describe('remove', () => { // }) }) -// describe('slice', () => { -// it('should return the generated content between the specified original characters', () => { -// const s = new MagicString('abcdefghijkl') +describe('slice', () => { + it('should return the generated content between the specified original characters', () => { + const s = new MagicString('abcdefghijkl') -// assert.equal(s.slice(3, 9), 'defghi') -// s.overwrite(4, 8, 'XX') -// assert.equal(s.slice(3, 9), 'dXXi') -// s.overwrite(2, 10, 'ZZ') -// assert.equal(s.slice(1, 11), 'bZZk') -// assert.equal(s.slice(2, 10), 'ZZ') + assert.equal(s.slice(3, 9), 'defghi') + s.overwrite(4, 8, 'XX') + assert.equal(s.slice(3, 9), 'dXXi') + s.overwrite(2, 10, 'ZZ') + assert.equal(s.slice(1, 11), 'bZZk') + assert.equal(s.slice(2, 10), 'ZZ') -// assert.throws(() => s.slice(3, 9)) -// }) + assert.throws(() => s.slice(3, 9)) + }) -// it('defaults `end` to the original string length', () => { -// const s = new MagicString('abcdefghijkl') -// assert.equal(s.slice(3), 'defghijkl') -// }) + // it('defaults `end` to the original string length', () => { + // const s = new MagicString('abcdefghijkl') + // assert.equal(s.slice(3), 'defghijkl') + // }) -// it('allows negative numbers as arguments', () => { -// const s = new MagicString('abcdefghijkl') -// assert.equal(s.slice(-3), 'jkl') -// assert.equal(s.slice(0, -3), 'abcdefghi') -// }) + it('allows negative numbers as arguments', () => { + const s = new MagicString('abcdefghijkl') + // assert.equal(s.slice(-3), 'jkl') + assert.equal(s.slice(0, -3), 'abcdefghi') + }) -// it('includes inserted characters, respecting insertion direction', () => { -// const s = new MagicString('abefij') + it('includes inserted characters, respecting insertion direction', () => { + const s = new MagicString('abefij') -// s.prependRight(2, 'cd') -// s.appendLeft(4, 'gh') + s.prependRight(2, 'cd') + s.appendLeft(4, 'gh') -// assert.equal(s.slice(), 'abcdefghij') -// assert.equal(s.slice(1, 5), 'bcdefghi') -// assert.equal(s.slice(2, 4), 'cdefgh') -// assert.equal(s.slice(3, 4), 'fgh') -// assert.equal(s.slice(0, 2), 'ab') -// assert.equal(s.slice(0, 3), 'abcde') -// assert.equal(s.slice(4, 6), 'ij') -// assert.equal(s.slice(3, 6), 'fghij') -// }) + // assert.equal(s.slice(), 'abcdefghij') + assert.equal(s.slice(1, 5), 'bcdefghi') + assert.equal(s.slice(2, 4), 'cdefgh') + assert.equal(s.slice(3, 4), 'fgh') + assert.equal(s.slice(0, 2), 'ab') + assert.equal(s.slice(0, 3), 'abcde') + assert.equal(s.slice(4, 6), 'ij') + assert.equal(s.slice(3, 6), 'fghij') + }) -// it('supports characters moved outward', () => { -// const s = new MagicString('abcdEFghIJklmn') + // it('supports characters moved outward', () => { + // const s = new MagicString('abcdEFghIJklmn') -// s.move(4, 6, 2) -// s.move(8, 10, 12) -// assert.equal(s.toString(), 'abEFcdghklIJmn') + // s.move(4, 6, 2) + // s.move(8, 10, 12) + // assert.equal(s.toString(), 'abEFcdghklIJmn') -// assert.equal(s.slice(1, -1), 'bEFcdghklIJm') -// assert.equal(s.slice(2, -2), 'cdghkl') -// assert.equal(s.slice(3, -3), 'dghk') -// assert.equal(s.slice(4, -4), 'EFcdghklIJ') -// assert.equal(s.slice(5, -5), 'FcdghklI') -// assert.equal(s.slice(6, -6), 'gh') -// }) + // assert.equal(s.slice(1, -1), 'bEFcdghklIJm') + // assert.equal(s.slice(2, -2), 'cdghkl') + // assert.equal(s.slice(3, -3), 'dghk') + // assert.equal(s.slice(4, -4), 'EFcdghklIJ') + // assert.equal(s.slice(5, -5), 'FcdghklI') + // assert.equal(s.slice(6, -6), 'gh') + // }) -// it('supports characters moved inward', () => { -// const s = new MagicString('abCDefghijKLmn') + // it('supports characters moved inward', () => { + // const s = new MagicString('abCDefghijKLmn') -// s.move(2, 4, 6) -// s.move(10, 12, 8) -// assert.equal(s.toString(), 'abefCDghKLijmn') - -// assert.equal(s.slice(1, -1), 'befCDghKLijm') -// assert.equal(s.slice(2, -2), 'CDghKL') -// assert.equal(s.slice(3, -3), 'DghK') -// assert.equal(s.slice(4, -4), 'efCDghKLij') -// assert.equal(s.slice(5, -5), 'fCDghKLi') -// assert.equal(s.slice(6, -6), 'gh') -// }) + // s.move(2, 4, 6) + // s.move(10, 12, 8) + // assert.equal(s.toString(), 'abefCDghKLijmn') -// it('supports characters moved opposing', () => { -// const s = new MagicString('abCDefghIJkl') - -// s.move(2, 4, 8) -// s.move(8, 10, 4) -// assert.equal(s.toString(), 'abIJefghCDkl') - -// assert.equal(s.slice(1, -1), 'bIJefghCDk') -// assert.equal(s.slice(2, -2), '') -// assert.equal(s.slice(3, -3), '') -// assert.equal(s.slice(-3, 3), 'JefghC') -// assert.equal(s.slice(4, -4), 'efgh') -// assert.equal(s.slice(0, 3), 'abIJefghC') -// assert.equal(s.slice(3), 'Dkl') -// assert.equal(s.slice(0, -3), 'abI') -// assert.equal(s.slice(-3), 'JefghCDkl') -// }) + // assert.equal(s.slice(1, -1), 'befCDghKLijm') + // assert.equal(s.slice(2, -2), 'CDghKL') + // assert.equal(s.slice(3, -3), 'DghK') + // assert.equal(s.slice(4, -4), 'efCDghKLij') + // assert.equal(s.slice(5, -5), 'fCDghKLi') + // assert.equal(s.slice(6, -6), 'gh') + // }) -// it('errors if replaced characters are used as slice anchors', () => { -// const s = new MagicString('abcdef') -// s.overwrite(2, 4, 'CD') + // it('supports characters moved opposing', () => { + // const s = new MagicString('abCDefghIJkl') + + // s.move(2, 4, 8) + // s.move(8, 10, 4) + // assert.equal(s.toString(), 'abIJefghCDkl') + + // assert.equal(s.slice(1, -1), 'bIJefghCDk') + // assert.equal(s.slice(2, -2), '') + // assert.equal(s.slice(3, -3), '') + // assert.equal(s.slice(-3, 3), 'JefghC') + // assert.equal(s.slice(4, -4), 'efgh') + // assert.equal(s.slice(0, 3), 'abIJefghC') + // assert.equal(s.slice(3), 'Dkl') + // assert.equal(s.slice(0, -3), 'abI') + // assert.equal(s.slice(-3), 'JefghCDkl') + // }) -// assert.throws(() => s.slice(2, 3), /slice end anchor/) + // it('errors if replaced characters are used as slice anchors', () => { + // const s = new MagicString('abcdef') + // s.overwrite(2, 4, 'CD') -// assert.throws(() => s.slice(3, 4), /slice start anchor/) + // assert.throws(() => s.slice(2, 3), /slice end anchor/) -// assert.throws(() => s.slice(3, 5), /slice start anchor/) + // assert.throws(() => s.slice(3, 4), /slice start anchor/) -// assert.equal(s.slice(1, 5), 'bCDe') -// }) + // assert.throws(() => s.slice(3, 5), /slice start anchor/) -// it('does not error if slice is after removed characters', () => { -// const s = new MagicString('abcdef') + // assert.equal(s.slice(1, 5), 'bCDe') + // }) -// s.remove(0, 2) + it('does not error if slice is after removed characters', () => { + const s = new MagicString('abcdef') -// assert.equal(s.slice(2, 4), 'cd') -// }) -// }) + s.remove(0, 2) -// describe('snip', () => { -// it('should return a clone with content outside `start` and `end` removed', () => { -// const s = new MagicString('abcdefghijkl', { -// filename: 'foo.js', -// }) + assert.equal(s.slice(2, 4), 'cd') + }) +}) -// s.overwrite(6, 9, 'GHI') +describe('snip', () => { + it('should return a clone with content outside `start` and `end` removed', () => { + /* const s = new MagicString('abcdefghijkl', { + filename: 'foo.js', + }) */ + const s = new MagicString('abcdefghijkl') -// const snippet = s.snip(3, 9) -// assert.equal(snippet.toString(), 'defGHI') -// assert.equal(snippet.filename, 'foo.js') -// }) + s.overwrite(6, 9, 'GHI') -// it('should snip from the start', () => { -// const s = new MagicString('abcdefghijkl') -// const snippet = s.snip(0, 6) + const snippet = s.snip(3, 9) + assert.equal(snippet.toString(), 'defGHI') + // assert.equal(snippet.filename, 'foo.js') + }) -// assert.equal(snippet.toString(), 'abcdef') -// }) + it('should snip from the start', () => { + const s = new MagicString('abcdefghijkl') + const snippet = s.snip(0, 6) -// it('should snip from the end', () => { -// const s = new MagicString('abcdefghijkl') -// const snippet = s.snip(6, 12) + assert.equal(snippet.toString(), 'abcdef') + }) -// assert.equal(snippet.toString(), 'ghijkl') -// }) + it('should snip from the end', () => { + const s = new MagicString('abcdefghijkl') + const snippet = s.snip(6, 12) -// it('should respect original indices', () => { -// const s = new MagicString('abcdefghijkl') -// const snippet = s.snip(3, 9) + assert.equal(snippet.toString(), 'ghijkl') + }) -// snippet.overwrite(6, 9, 'GHI') -// assert.equal(snippet.toString(), 'defGHI') -// }) -// }) + it('should respect original indices', () => { + const s = new MagicString('abcdefghijkl') + const snippet = s.snip(3, 9) + + snippet.overwrite(6, 9, 'GHI') + assert.equal(snippet.toString(), 'defGHI') + }) +}) describe('trim', () => { it('should trim original content', () => {