use super::{format_page, short_title};
use crate::formats::FileFormat;
pub const TAGLINE: &str = "Explore tabular data in your terminal";
pub const PITCH: &str = "Parquet, CSV, JSON, Arrow, Excel, SQLite, logs, audio, model files and more, \
including binary formats you describe in a format spec.";
pub fn summary() -> String {
format!("{TAGLINE}: Parquet, CSV, JSON and more")
}
pub fn description() -> String {
format!(
"{PITCH} On disk or in S3, GCS, Azure and HTTP(S). \
Query with SQL or q, sort and filter, chart, analyze and export."
)
}
pub fn toml_string(text: &str) -> String {
format!("\"{}\"", text.replace('\\', "\\\\").replace('"', "\\\""))
}
fn html(text: &str) -> String {
text.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}
struct Channel {
id: String,
label: String,
link: Option<String>,
os: Vec<String>,
lang: String,
command: Option<String>,
anchor: Option<String>,
text: Option<String>,
html: Option<String>,
table: bool,
}
fn channels(read: &dyn Fn(&str) -> String) -> Vec<Channel> {
let doc: toml::Table = read("scripts/docs/install.toml")
.parse()
.unwrap_or_else(|e| panic!("scripts/docs/install.toml: {e}"));
let list = doc
.get("channel")
.and_then(|c| c.as_array())
.expect("install.toml: [[channel]]");
list.iter()
.map(|c| {
let s = |key: &str| c.get(key).and_then(|v| v.as_str()).map(str::to_string);
let channel = Channel {
id: s("id").expect("install.toml: a channel without `id`"),
label: s("label").expect("install.toml: a channel without `label`"),
link: s("link"),
os: c
.get("os")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|o| o.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default(),
lang: s("lang").unwrap_or_else(|| "bash".into()),
command: s("command"),
anchor: s("anchor"),
text: s("text"),
html: s("html"),
table: c.get("table").and_then(|v| v.as_bool()).unwrap_or(true),
};
assert!(
channel.command.is_some() || (channel.text.is_some() && channel.html.is_some()),
"install.toml: `{}` needs a command, or both text and html",
channel.id
);
channel
})
.collect()
}
fn headline(read: &dyn Fn(&str) -> String) -> String {
channels(read)
.into_iter()
.filter(|c| !c.table)
.filter_map(|c| {
c.command
.map(|cmd| format!("```{},install\n{}\n```\n", c.lang, cmd))
})
.collect::<Vec<_>>()
.join("\n")
}
fn table(read: &dyn Fn(&str) -> String, install_page: &str) -> String {
let mut out = String::from("| Platform | Command |\n|---|---|\n");
for c in channels(read).into_iter().filter(|c| c.table) {
let label = match &c.link {
Some(link) => format!("[{}]({link})", c.label),
None => c.label.clone(),
};
let cell = match (&c.command, &c.anchor) {
(Some(cmd), _) if !cmd.contains('\n') => format!("`{cmd}`"),
(Some(_), Some(anchor)) => {
let name = anchor.replace('-', " ");
let mut letters = name.chars();
let first = letters.next().map(|l| l.to_ascii_uppercase());
format!(
"[{}{}]({install_page}#{anchor})",
first.unwrap_or_default(),
letters.as_str()
)
}
(Some(_), None) => panic!("install.toml: `{}` has lines but no anchor", c.id),
(None, _) => c.text.clone().unwrap_or_default(),
};
out.push_str(&format!("| {label} | {} |\n", cell.replace('|', "\\|")));
}
out
}
pub fn readme_install(read: &dyn Fn(&str) -> String) -> String {
format!(
"{}\n{}",
headline(read),
table(
read,
"https://derekwisong.github.io/datui/latest/getting-started/installation.html"
)
)
}
pub fn channel_block(read: &dyn Fn(&str) -> String, id: &str) -> String {
let c = channels(read)
.into_iter()
.find(|c| c.id == id)
.unwrap_or_else(|| panic!("install.toml: no channel `{id}`"));
let cmd = c
.command
.unwrap_or_else(|| panic!("install.toml: `{id}` has no command"));
format!("```{},install\n{}\n```\n", c.lang, cmd)
}
pub fn docs_install_script(read: &dyn Fn(&str) -> String) -> String {
headline(read)
}
pub fn docs_install_table(read: &dyn Fn(&str) -> String) -> String {
table(read, "")
}
pub fn landing_install(read: &dyn Fn(&str) -> String) -> String {
let mut out = String::from("<div class=\"install-panels\" id=\"install-panels\">\n");
for c in channels(read) {
out.push_str(&format!(
" <section class=\"install-panel\" id=\"install-{}\" data-label=\"{}\" data-os=\"{}\">\n <h3>{}</h3>\n",
html(&c.id),
html(&c.label),
html(&c.os.join(" ")),
html(&c.label),
));
match &c.command {
Some(cmd) => out.push_str(&format!(
" <pre data-example=\"{},install\"><code>{}</code></pre>\n",
html(&c.lang),
html(cmd)
)),
None => out.push_str(&format!(
" <p>{}</p>\n",
c.html.as_deref().unwrap_or_default()
)),
}
out.push_str(" </section>\n");
}
out.push_str("</div>");
out
}
fn family_title(page: &str) -> &'static str {
match page {
"columnar-and-json.md" => "Columnar and JSON",
"delimited-text.md" => "Delimited text",
"databases-and-arrays.md" => "Databases and arrays",
"model-files.md" => "Model files",
"signals-and-logs.md" => "Signals and logs",
_ => panic!("no family title for {page}"),
}
}
pub fn landing_formats() -> String {
let mut families: Vec<(&str, Vec<&str>)> = Vec::new();
for format in FileFormat::ALL {
let page = format_page(format).split('#').next().unwrap_or_default();
match families.iter_mut().find(|(p, _)| *p == page) {
Some((_, names)) => names.push(short_title(format)),
None => families.push((page, vec![short_title(format)])),
}
}
let mut out = String::from("<dl class=\"format-families\">\n");
for (page, names) in families {
out.push_str(&format!(
" <div><dt><a href=\"{{{{ guide('formats/{}') }}}}\">{}</a></dt><dd>{}</dd></div>\n",
page.trim_end_matches(".md"),
family_title(page),
html(&names.join(" ยท ")),
));
}
out.push_str(
" <div><dt><a href=\"{{ guide('formats/format-specs') }}\">Format specs</a></dt><dd>Your own binary formats, described in TOML</dd></div>\n</dl>",
);
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_channel_reads_and_one_heads_the_tables() {
let root = super::super::repo_root();
let all = channels(&|p| super::super::read_file(&root, p));
assert_eq!(all.iter().filter(|c| !c.table).count(), 1);
for os in ["linux", "macos", "windows"] {
assert!(
all.iter().any(|c| c.os.iter().any(|o| o == os)),
"no channel shown first on {os}"
);
}
}
#[test]
fn every_format_is_in_the_strip() {
let strip = landing_formats();
for format in FileFormat::ALL {
assert!(strip.contains(short_title(format)), "{format:?}");
}
}
#[test]
fn the_summary_fits_every_package() {
assert!(summary().len() <= 80, "{}", summary());
assert!(!summary().ends_with('.'));
}
}