///|
fn render_block(
out : StringBuilder,
block : Block,
depth : Int32,
) -> Unit raise JcampError {
guard depth <= Limits::default().max_depth else {
reject("LIMIT_DEPTH", "rendering depth budget exceeded")
}
for record in block.records {
append_record(out, record)
}
for table in block.tables {
append_record(out, {
label: table.label,
value: table.descriptor,
line: table.line,
})
for row in table.rows {
guard !trim(row.text).has_prefix("##") &&
!row.text.contains("\n") &&
!row.text.contains("\r") else {
reject(
"WRITE_INJECTION",
"data row would inject a label or extra line",
line=row.line,
)
}
out.write_string(row.text + "\n")
}
}
for child in block.children {
render_block(out, child, depth + 1)
}
out.write_string("##END=\n")
}
///|
pub fn render_document(
document : Document,
read_options~ : ParseOptions = ParseOptions::default(),
) -> String raise JcampError {
ignore(spectra_from_document(document, options=read_options))
let out = StringBuilder()
for block in document.blocks {
render_block(out, block, 1)
}
let text = out.to_string()
ignore(read_spectra(text, options=read_options))
text
}
///|
pub fn write_collection(
spectra : Array[Spectrum],
title~ : String = "Spectral collection",
options~ : WriteOptions = WriteOptions::default(),
) -> String raise JcampError {
guard !spectra.is_empty() && spectra.length() < Limits::default().max_blocks else {
reject(
"COLLECTION_SIZE", "collection requires a bounded nonempty list of spectra",
)
}
let out = StringBuilder()
for
record in [
{ label: "TITLE", value: title, line: 0, },
{ label: "JCAMP-DX", value: "5.01", line: 0, },
{ label: "DATA TYPE", value: "LINK", line: 0, },
{ label: "BLOCKS", value: spectra.length().to_string(), line: 0, },
] {
append_record(out, record)
}
let mut total = 0
for spectrum in spectra {
guard spectrum.points.length() <= Limits::default().max_points - total else {
reject("LIMIT_POINTS", "collection point budget exceeded")
}
total += spectrum.points.length()
out.write_string(write_spectrum(spectrum, options~))
}
out.write_string("##END=\n")
out.to_string()
}
///|
pub fn combine_documents(
documents : Array[Document],
title~ : String = "Combined documents",
read_options~ : ParseOptions = ParseOptions::default(),
) -> Document raise JcampError {
guard !documents.is_empty() else {
reject("COLLECTION_EMPTY", "no documents to combine")
}
let children : Array[Block] = []
for document in documents {
ignore(spectra_from_document(document, options=read_options))
for block in document.blocks {
guard children.length() + 1 < Limits::default().max_blocks else {
reject("LIMIT_BLOCKS", "combined root block budget exceeded")
}
children.push(block)
}
}
let root : Block = {
records: [
{ label: "TITLE", value: title, line: 0, },
{ label: "JCAMP-DX", value: "5.01", line: 0, },
{ label: "DATA TYPE", value: "LINK", line: 0, },
{ label: "BLOCKS", value: children.length().to_string(), line: 0, },
],
tables: [],
children,
start_line: 0,
end_line: 0,
}
let result : Document = { blocks: [root], }
ignore(spectra_from_document(result, options=read_options))
result
}
///|
pub fn split_document(
document : Document,
read_options~ : ParseOptions = ParseOptions::default(),
) -> Array[Document] raise JcampError {
ignore(spectra_from_document(document, options=read_options))
let result : Array[Document] = []
fn visit(
block : Block,
ancestors : Array[Array[Record]],
depth : Int32,
) -> Unit raise JcampError {
guard depth <= Limits::default().max_depth else {
reject("LIMIT_DEPTH", "splitting depth budget exceeded")
}
if canonical_label(block_kind(block)) == "LINK" {
let chain = ancestors + [block.records]
for child in block.children {
visit(child, chain, depth + 1)
}
} else {
let records = if ancestors.is_empty() {
block.records.copy()
} else {
guard record_value(block.records, "$MOONJCAMP.CONTAINERS") is None else {
reject(
"PROVENANCE_COLLISION",
"source already contains reserved container provenance",
line=block.start_line,
)
}
set_record(
block.records,
"$MOONJCAMP.CONTAINERS",
Json(ancestors).stringify(),
)
}
result.push({ blocks: [{ ..block, records, }], })
}
}
for block in document.blocks {
visit(block, [], 1)
}
result
}
///|
pub fn rewrite_document(
document : Document,
options~ : WriteOptions = WriteOptions::default(),
read_options~ : ParseOptions = ParseOptions::default(),
) -> String raise JcampError {
ignore(spectra_from_document(document, options=read_options))
let out = StringBuilder()
fn visit(block : Block, depth : Int32) -> Unit raise JcampError {
guard depth <= Limits::default().max_depth else {
reject("LIMIT_DEPTH", "rewriting depth budget exceeded")
}
if canonical_label(block_kind(block)) == "LINK" {
for record in block.records {
append_record(out, record)
}
for child in block.children {
visit(child, depth + 1)
}
out.write_string("##END=\n")
} else {
out.write_string(
write_spectrum(
spectrum_from_block(block, options=read_options),
options~,
),
)
}
}
for block in document.blocks {
visit(block, 1)
}
out.to_string()
}