Skip to content

Commit 45e8b3c

Browse files
committed
basic impl
1 parent 52656d5 commit 45e8b3c

File tree

9 files changed

+304
-0
lines changed

9 files changed

+304
-0
lines changed

.vscode/settings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"cSpell.words": [
3+
"outro"
4+
]
5+
}

Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "string_wizard"
3+
version = "0.0.1"
4+
edition = "2021"
5+
license = "MIT"
6+
7+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8+
9+
[dependencies]
10+
index_vec = { version = "0.1.3" }
11+
rustc-hash = { version = "1.1.0" }
12+
text-size = "1.1.1"

src/chunk.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use crate::{CowStr, span::Span, ChunkIdx};
2+
3+
#[derive(Debug, Default)]
4+
pub struct Chunk<'s> {
5+
intro: Vec<CowStr<'s>>,
6+
span: Span,
7+
outro: Vec<CowStr<'s>>,
8+
len: usize,
9+
pub(crate) next: Option<ChunkIdx>
10+
}
11+
12+
impl<'s> Chunk<'s> {
13+
pub fn new(span: Span) -> Self {
14+
Self {
15+
len: span.size(),
16+
span,
17+
..Default::default()
18+
}
19+
}
20+
}
21+
22+
impl<'s> Chunk<'s> {
23+
pub fn fragments(&'s self, original_source: &'s CowStr<'s>) -> impl Iterator<Item = &'s str> {
24+
let intro_iter = self.intro.iter().map(|frag| frag.as_ref());
25+
let source_frag = self.span.text(original_source.as_ref());
26+
let outro_iter = self.outro.iter().map(|frag| frag.as_ref());
27+
intro_iter.chain(Some(source_frag)).chain(outro_iter)
28+
}
29+
30+
pub fn append_outro(&mut self, frag: CowStr<'s>) {
31+
self.len += frag.len();
32+
self.outro.push(frag.into())
33+
}
34+
35+
pub fn len(&self) -> usize {
36+
self.len
37+
}
38+
}

src/joiner.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use crate::{MagicString, CowStr};
2+
3+
#[derive(Default)]
4+
pub struct Joiner<'s> {
5+
sources: Vec<MagicString<'s>>,
6+
}
7+
8+
impl<'s> Joiner<'s> {
9+
// --- public
10+
pub fn append(&mut self, source: MagicString<'s>) -> &mut Self {
11+
self.sources.push(source);
12+
self
13+
}
14+
15+
pub fn append_raw(&mut self, raw: impl Into<CowStr<'s>>) -> &mut Self {
16+
self.sources.push(MagicString::new(raw));
17+
self
18+
}
19+
20+
pub fn len(&self) -> usize {
21+
self.sources.iter().map(|s| s.len()).sum()
22+
}
23+
24+
pub fn join(&self) -> String {
25+
let mut ret = String::with_capacity(self.len());
26+
self.fragments().for_each(|frag| {
27+
ret.push_str(frag);
28+
});
29+
ret
30+
}
31+
32+
// --- private
33+
34+
fn fragments(&'s self) -> impl Iterator<Item = &'s str> {
35+
self.sources.iter().flat_map(|c| c.fragments())
36+
}
37+
38+
39+
}

src/lib.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
2+
mod chunk;
3+
mod span;
4+
mod joiner;
5+
mod magic_string;
6+
use std::borrow::Cow;
7+
8+
type CowStr<'s> = Cow<'s, str>;
9+
10+
use chunk::Chunk;
11+
use index_vec::IndexVec;
12+
13+
pub use crate::{magic_string::{MagicString, MagicStringOptions}, joiner::Joiner};
14+
15+
16+
index_vec::define_index_type! {
17+
struct ChunkIdx = u32;
18+
}
19+
20+
type ChunkVec<'s> = IndexVec<ChunkIdx, Chunk<'s>>;
21+
22+
index_vec::define_index_type! {
23+
struct SourceIdx = u32;
24+
}
25+
26+
27+
28+
pub struct Bundle {}

src/magic_string.rs

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
use rustc_hash::FxHashMap;
2+
3+
use crate::{chunk::Chunk, span::Span, ChunkIdx, ChunkVec, CowStr};
4+
5+
#[derive(Debug, Default)]
6+
pub struct MagicStringOptions {
7+
pub filename: Option<String>,
8+
}
9+
10+
#[derive(Default)]
11+
pub struct MagicString<'s> {
12+
source: CowStr<'s>,
13+
source_len: u32,
14+
chunks: ChunkVec<'s>,
15+
first_chunk: Option<ChunkIdx>,
16+
chunk_by_start: FxHashMap<u32, ChunkIdx>,
17+
chunk_by_end: FxHashMap<u32, ChunkIdx>,
18+
pub filename: Option<String>,
19+
}
20+
21+
impl<'s> MagicString<'s> {
22+
// --- public
23+
24+
pub fn new(source: impl Into<CowStr<'s>>) -> Self {
25+
Self::with_options(source, Default::default())
26+
}
27+
28+
pub fn with_options(source: impl Into<CowStr<'s>>, options: MagicStringOptions) -> Self {
29+
let source = source.into();
30+
let source_len: u32 = source.len().try_into().expect("source is too big");
31+
let mut magic_string = Self {
32+
source,
33+
source_len,
34+
..Default::default()
35+
};
36+
magic_string.split_at(source_len);
37+
magic_string.first_chunk = Some(ChunkIdx::from_raw(0));
38+
39+
magic_string.filename = options.filename;
40+
41+
magic_string
42+
}
43+
44+
pub fn append(&mut self, source: impl Into<CowStr<'s>>) -> &mut Self {
45+
self.last_chunk_mut().append_outro(source.into());
46+
self
47+
}
48+
49+
pub fn len(&self) -> usize {
50+
self.chunks.iter().map(|c| c.len()).sum()
51+
}
52+
53+
pub fn to_string(&self) -> String {
54+
let size_hint = self.len();
55+
let mut ret = String::with_capacity(size_hint);
56+
self.fragments().for_each(|f| ret.push_str(f));
57+
ret
58+
}
59+
60+
// --- private
61+
62+
fn iter_chunks(&self) -> impl Iterator<Item = &Chunk> {
63+
ChunkIter {
64+
next: self.first_chunk,
65+
chunks: &self.chunks,
66+
}
67+
}
68+
69+
pub(crate) fn fragments(&'s self) -> impl Iterator<Item = &'s str> {
70+
self.iter_chunks().flat_map(|c| c.fragments(&self.source))
71+
}
72+
73+
/// a b c d e f g
74+
/// 0 1 2 3 4 5 6
75+
/// split_at(3)
76+
/// a b c d e f g
77+
/// [0 1 2][3 4 5 6]
78+
fn split_at(&mut self, idx: u32) {
79+
let source_len = self.source_len;
80+
debug_assert!(idx <= source_len);
81+
if self.chunks.is_empty() {
82+
let prev_span = Span(0, idx);
83+
self.create_chunk(prev_span);
84+
if idx < source_len {
85+
self.create_chunk(Span(idx, source_len));
86+
}
87+
} else {
88+
}
89+
}
90+
91+
fn create_chunk(&mut self, span: Span) -> ChunkIdx {
92+
let idx = self.chunks.push(Chunk::new(span));
93+
self.chunk_by_start.insert(span.start(), idx);
94+
self.chunk_by_end.insert(span.end(), idx);
95+
idx
96+
}
97+
98+
fn last_chunk_mut(&mut self) -> &mut Chunk<'s> {
99+
let idx = self.chunk_by_end.get(&(self.source_len)).unwrap();
100+
&mut self.chunks[*idx]
101+
}
102+
}
103+
104+
struct ChunkIter<'a> {
105+
next: Option<ChunkIdx>,
106+
chunks: &'a ChunkVec<'a>,
107+
}
108+
109+
impl<'a> Iterator for ChunkIter<'a> {
110+
type Item = &'a Chunk<'a>;
111+
112+
fn next(&mut self) -> Option<Self::Item> {
113+
self.next.take().map(|next| {
114+
let chunk = &self.chunks[next];
115+
self.next = chunk.next;
116+
117+
chunk
118+
})
119+
}
120+
}

src/span.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#[derive(Debug, Default, Clone, Copy)]
2+
pub struct Span(pub u32, pub u32);
3+
4+
impl Span {
5+
pub fn start(&self) -> u32 {
6+
self.0
7+
}
8+
9+
pub fn end(&self) -> u32 {
10+
self.1
11+
}
12+
13+
pub fn text<'s>(&self, source: &'s str) -> &'s str {
14+
&source[self.start() as usize..self.end() as usize]
15+
}
16+
17+
pub fn size(&self) -> usize {
18+
(self.1 - self.0) as usize
19+
}
20+
}

tests/joiner.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use string_wizard::{Joiner, MagicString};
2+
mod append {
3+
use super::*;
4+
5+
#[test]
6+
fn should_append_content() {
7+
let mut j = Joiner::default();
8+
j.append(MagicString::new("*"));
9+
j.append_raw("123").append_raw("456");
10+
assert_eq!(j.join(), "*123456");
11+
}
12+
}

tests/magic_string.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
use string_wizard::MagicString;
2+
3+
use string_wizard::MagicStringOptions;
4+
mod options {
5+
use super::*;
6+
#[test]
7+
fn stores_source_file_information() {
8+
let s = MagicString::with_options(
9+
"abc",
10+
MagicStringOptions {
11+
filename: Some("foo.js".to_string()),
12+
},
13+
);
14+
assert_eq!(s.filename, Some("foo.js".to_string()))
15+
}
16+
}
17+
18+
mod append {
19+
use super::*;
20+
21+
#[test]
22+
fn should_append_content() {
23+
// should append content
24+
let mut s = MagicString::new("abcdefghijkl");
25+
s.append("xyz");
26+
assert_eq!(s.to_string(), "abcdefghijklxyz");
27+
s.append("xyz");
28+
assert_eq!(s.to_string(), "abcdefghijklxyzxyz");
29+
}
30+
}

0 commit comments

Comments
 (0)