///|
/// The content of one file in a document set.
pub(all) enum FileContent {
Text(String)
Bytes(Bytes)
} derive(Eq, Debug)
///|
/// One file of a document set: its store path and its content.
pub(all) struct FileEntry {
path : StorePath
content : FileContent
} derive(Eq, Debug)
///|
/// An immutable set of files in memory, sorted by path. It is the input of the
/// pure core: stores read into a document set, and the core loads bundles from it.
struct DocumentSet {
entries : ReadOnlyArray[FileEntry]
by_path : Map[StorePath, Int32]
}
///|
/// Builds a document set. A later entry for the same path replaces an earlier one.
pub fn DocumentSet::DocumentSet(entries : ArrayView[FileEntry]) -> DocumentSet {
let latest : Map[StorePath, FileEntry] = Map([])
for entry in entries {
latest[entry.path] = entry
}
let sorted = latest.values().collect()
sorted.sort_by((a, b) => a.path.compare(b.path))
let by_path : Map[StorePath, Int32] = Map([])
for i, entry in sorted {
by_path[entry.path] = i
}
{ entries: ReadOnlyArray::from_array(sorted), by_path, }
}
///|
/// Builds a document set from `(path, text)` pairs.
pub fn DocumentSet::of_texts(
files : ArrayView[(String, String)],
) -> DocumentSet raise PathError {
let entries = []
for file in files {
entries.push({ path: StorePath(file.0), content: Text(file.1), })
}
DocumentSet(entries)
}
///|
/// All files, sorted by path.
pub fn DocumentSet::entries(self : DocumentSet) -> ReadOnlyArray[FileEntry] {
self.entries
}
///|
pub fn DocumentSet::get(self : DocumentSet, path : StorePath) -> FileEntry? {
match self.by_path.get(path) {
Some(i) => Some(self.entries[i])
None => None
}
}
///|
pub fn DocumentSet::contains(self : DocumentSet, path : StorePath) -> Bool {
self.by_path.contains(path)
}
///|
/// True when a file exists under the directory `dir`.
pub fn DocumentSet::has_directory(self : DocumentSet, dir : StoreDir) -> Bool {
self.entries.any(e => e.path.is_under(dir))
}
///|
/// A new document set with `entry` added or replaced.
pub fn DocumentSet::with_entry(
self : DocumentSet,
entry : FileEntry,
) -> DocumentSet {
let entries = self.entries.iter().filter(e => e.path != entry.path).collect()
entries.push(entry)
DocumentSet(entries)
}
///|
/// A new document set without the file at `path`.
pub fn DocumentSet::without(
self : DocumentSet,
path : StorePath,
) -> DocumentSet {
DocumentSet(self.entries.iter().filter(e => e.path != path).collect())
}
///|
/// The number of files.
pub fn DocumentSet::length(self : DocumentSet) -> Int32 {
self.entries.length()
}
///|
/// The text of a file, decoding bytes as UTF-8 when needed.
pub fn FileContent::text(self : FileContent) -> String {
match self {
Text(text) => text
Bytes(bytes) => decode_utf8(bytes)
}
}
///|
/// Decodes UTF-8, replacing invalid sequences with U+FFFD.
fn decode_utf8(bytes : Bytes) -> String {
let out = StringBuilder()
let n = bytes.length()
let mut i = 0
while i < n {
let b0 = bytes[i].to_int()
let (code, size) = if b0 < 0x80 {
(b0, 1)
} else if b0 >= 0xC2 && b0 < 0xE0 && i + 1 < n {
(((b0 & 0x1F) << 6) | (bytes[i + 1].to_int() & 0x3F), 2)
} else if b0 >= 0xE0 && b0 < 0xF0 && i + 2 < n {
(
((b0 & 0x0F) << 12) |
((bytes[i + 1].to_int() & 0x3F) << 6) |
(bytes[i + 2].to_int() & 0x3F),
3,
)
} else if b0 >= 0xF0 && b0 < 0xF5 && i + 3 < n {
(
((b0 & 0x07) << 18) |
((bytes[i + 1].to_int() & 0x3F) << 12) |
((bytes[i + 2].to_int() & 0x3F) << 6) |
(bytes[i + 3].to_int() & 0x3F),
4,
)
} else {
(0xFFFD, 1)
}
out.write_char(Int32::unsafe_to_char(code))
i += size
}
out.to_string()
}
///|
/// Encodes text as UTF-8.
pub fn encode_utf8(text : String) -> Bytes {
let out = Buffer()
for ch in text {
out.write_char_utf8(ch)
}
out.to_bytes()
}
///|
pub extend FileContent with Eq::{not_equal, equal}
///|
pub extend FileContent with @debug.Debug::{to_repr}
///|
pub extend FileEntry with Eq::{not_equal, equal}
///|
pub extend FileEntry with @debug.Debug::{to_repr}