diff --git a/web/book/highlight-prql.js b/web/book/highlight-prql.js index bc78c7c7d511..e7626be1024f 100644 --- a/web/book/highlight-prql.js +++ b/web/book/highlight-prql.js @@ -205,8 +205,8 @@ formatting = function (hljs) { hljs.registerLanguage("prql", formatting); hljs.registerLanguage("prql_no_test", formatting); -hljs.registerLanguage("prql_error", formatting); -hljs.registerLanguage("prql_no_fmt", formatting); +hljs.registerLanguage("prql,error", formatting); +hljs.registerLanguage("prql,no-fmt", formatting); hljs.registerLanguage("elm", formatting); // These lines should only exists in the book, not the website. @@ -217,7 +217,7 @@ Array.from(document.querySelectorAll("code.language-prql")).forEach( (a) => console.log(a) || hljs.highlightBlock(a) ); -Array.from(document.querySelectorAll("code.language-prql_error")).forEach( +Array.from(document.querySelectorAll("code.language-prql,error")).forEach( (a) => console.log(a) || hljs.highlightBlock(a) ); diff --git a/web/book/src/examples/cte.md b/web/book/src/examples/cte.md index eab8c1592c7a..f0b119d65ada 100644 --- a/web/book/src/examples/cte.md +++ b/web/book/src/examples/cte.md @@ -1,4 +1,4 @@ -```prql_no_fmt +```prql no-fmt let newest_employees = ( from employees sort tenure diff --git a/web/book/src/examples/employees.md b/web/book/src/examples/employees.md index 48467dc87ecb..6ea1a138aba5 100644 --- a/web/book/src/examples/employees.md +++ b/web/book/src/examples/employees.md @@ -99,7 +99,7 @@ select [mng_name, managers.gender, salary_avg, salary_sd] > Find distributions of titles, salaries and genders for each department. -```prql_no_fmt +```prql no-fmt from de=dept_emp join s=salaries side:left [ (s.emp_no == de.emp_no), diff --git a/web/book/src/language-features/case.md b/web/book/src/language-features/case.md index f91768d21952..4fe611bb91c2 100644 --- a/web/book/src/language-features/case.md +++ b/web/book/src/language-features/case.md @@ -6,7 +6,7 @@ PRQL uses `case` for both SQL's `CASE` and `IF` statements. Here's an example: -```prql_no_fmt +```prql no-fmt from employees derive distance = case [ city == "Calgary" => 0, @@ -17,7 +17,7 @@ derive distance = case [ If no condition is met, the value takes a `null` value. To set a default, use a `true` condition: -```prql_no_fmt +```prql no-fmt from employees derive distance = case [ city == "Calgary" => 0, diff --git a/web/book/src/language-features/s-strings.md b/web/book/src/language-features/s-strings.md index 8cde2da4560d..6201d39594e1 100644 --- a/web/book/src/language-features/s-strings.md +++ b/web/book/src/language-features/s-strings.md @@ -61,7 +61,7 @@ should implement it in PRQL or PRQL's stdlib. To output braces from an s-string, use double braces: -```prql_no_fmt +```prql no-fmt from employees derive [ has_valid_title = s"regexp_contains(title, '([a-z0-9]*-){{2,}}')" diff --git a/web/book/src/language-features/strings.md b/web/book/src/language-features/strings.md index 23acb935ce77..7dc7ee8f392b 100644 --- a/web/book/src/language-features/strings.md +++ b/web/book/src/language-features/strings.md @@ -33,7 +33,7 @@ select x = """""I said """hello world"""!""""" Strings can also contain any escape defined by [JSON standard](https://www.ecma-international.org/publications-and-standards/standards/ecma-404/). -```prql_no_fmt +```prql no-fmt from my_table select x = "\t\tline ends here\n \\ " ``` diff --git a/web/book/src/language-features/target.md b/web/book/src/language-features/target.md index 9a7a87391457..f4496f7637b8 100644 --- a/web/book/src/language-features/target.md +++ b/web/book/src/language-features/target.md @@ -15,7 +15,7 @@ sort age take 10 ``` -```prql_no_fmt +```prql no-fmt prql target:sql.mssql from employees @@ -47,7 +47,7 @@ very welcome. PRQL allows specifying a version of the language in the PRQL header, like: -```prql_no_fmt +```prql no-fmt prql version:"0.6.1" from employees diff --git a/web/book/src/lib.rs b/web/book/src/lib.rs index 6f8a71c6ca7b..85f593d437a3 100644 --- a/web/book/src/lib.rs +++ b/web/book/src/lib.rs @@ -96,9 +96,11 @@ impl Preprocessor for ComparisonPreprocessor { } } -pub fn code_block_lang<'a>(event: &'a Event) -> Option<&'a str> { +/// Returns the language of a code block, divided by commas +/// For example: ```prql no-test +pub fn code_block_lang_tags(event: &Event) -> Option> { if let Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang))) = event { - Some(lang.as_ref()) + Some(lang.to_string().split(' ').map(|x| x.to_string()).collect()) } else { None } @@ -110,15 +112,22 @@ fn replace_examples(text: &str) -> Result { while let Some(event) = parser.next() { // If it's not PRQL, just push it and continue - let Some(lang) = code_block_lang(&event) else { + let Some(lang_tags) = code_block_lang_tags(&event) else { cmark_acc.push(event.to_owned()); continue; }; - if !lang.starts_with("prql") { + if !lang_tags.contains(&"prql".to_string()) { cmark_acc.push(event.to_owned()); continue; } + let allowed_tags = ["prql", "no-test", "no-fmt", "error"]; + for tag in lang_tags.iter() { + if !allowed_tags.contains(&tag.as_str()) { + bail!("Unknown code block language: {}", tag) + } + } + let Some(Event::Text(text)) = parser.next() else { bail!("Expected text within code block") }; @@ -127,17 +136,10 @@ fn replace_examples(text: &str) -> Result { let options = prql_compiler::Options::default().no_signature(); let result = compile(&prql, &options); - match lang { - "prql" | "prql_no_fmt" => cmark_acc.push(Event::Html( - table_of_comparison( - &prql, - result - .map_err(|_| anyhow::anyhow!("Query raised an error:\n\n {prql}\n\n"))? - .as_str(), - ) - .into(), - )), - "prql_error" => cmark_acc.push(Event::Html( + if lang_tags.contains(&"no-test".to_string()) { + cmark_acc.push(Event::Html(table_of_prql_only(&prql).into())); + } else if lang_tags.contains(&"error".to_string()) { + cmark_acc.push(Event::Html( table_of_error( &prql, result @@ -151,14 +153,19 @@ fn replace_examples(text: &str) -> Result { .as_str(), ) .into(), - )), - "prql_no_test" => { - cmark_acc.push(Event::Html(table_of_prql_only(&prql).into())); - } - _ => { - bail!("Unknown code block language: {}", lang) - } - }; + )) + } else { + // Either a bare `prql` or with `no-fmt` + cmark_acc.push(Event::Html( + table_of_comparison( + &prql, + result + .map_err(|_| anyhow::anyhow!("Query raised an error:\n\n {prql}\n\n"))? + .as_str(), + ) + .into(), + )) + } // Skip ending tag parser.next(); } @@ -270,7 +277,7 @@ from x import sys ``` -```prql_error +```prql error this is an error ``` "###; diff --git a/web/book/src/queries/functions.md b/web/book/src/queries/functions.md index 5f31e4de8903..e363c3578aee 100644 --- a/web/book/src/queries/functions.md +++ b/web/book/src/queries/functions.md @@ -16,7 +16,7 @@ Functions have two types of parameters: So this function is named `fahrenheit_to_celsius` and has one parameter `temp`: -```prql_no_fmt +```prql no-fmt func fahrenheit_to_celsius temp -> (temp - 32) / 1.8 from cities diff --git a/web/book/src/syntax.md b/web/book/src/syntax.md index 44cd64cbc4e0..9c43d9f6c40e 100644 --- a/web/book/src/syntax.md +++ b/web/book/src/syntax.md @@ -137,7 +137,7 @@ function call, like `foo + bar`. Here's a full rundown of times this applier: -```prql_no_fmt +```prql no-fmt from employees # Requires parentheses, because it's contains a pipe derive is_proximate = (distance | in 0..20) @@ -169,7 +169,7 @@ sort [-distance] This doesn't work, for example (though it should provide a much better error message): -```prql_error +```prql error from employees derive total_distance = sum distance ``` diff --git a/web/book/src/transforms/select.md b/web/book/src/transforms/select.md index b6f9694ef6d9..81f67de6c7fd 100644 --- a/web/book/src/transforms/select.md +++ b/web/book/src/transforms/select.md @@ -50,19 +50,19 @@ We can use `!` to exclude a list of columns. This can operate in two ways: Some examples: -```prql_no_fmt +```prql no-fmt prql target:sql.bigquery from tracks select ![milliseconds,bytes] ``` -```prql_no_fmt +```prql no-fmt from tracks select [track_id, title, composer, bytes] select ![title, composer] ``` -```prql_no_fmt +```prql no-fmt from artists derive nick = name select ![artists.*] diff --git a/web/book/tests/snapshot.rs b/web/book/tests/snapshot.rs index 6149f32287a4..1563b55f15e4 100644 --- a/web/book/tests/snapshot.rs +++ b/web/book/tests/snapshot.rs @@ -2,6 +2,7 @@ use anyhow::{bail, Result}; use globset::Glob; use insta::assert_snapshot; +use mdbook_prql::code_block_lang_tags; use prql_compiler::*; use std::path::{Path, PathBuf}; use std::{collections::HashMap, fs}; @@ -32,6 +33,10 @@ const ROOT_EXAMPLES_PATH: &str = "tests/prql"; /// Collect all the PRQL examples in the book, as a map of . fn collect_book_examples() -> Result> { + // TODO: instead of returning Strings with embedded tags (e.g. `# error`), + // we could instead return a struct with a `prql` field and a struct of its + // metadata. That would make `test_display` work by matching on the metadata + // rather than re-parsing the string. use pulldown_cmark::{Event, Parser}; let glob = Glob::new("**/*.md")?.compile_matcher(); let examples_in_book: HashMap = WalkDir::new(Path::new("./src/")) @@ -46,24 +51,25 @@ fn collect_book_examples() -> Result> { let mut parser = Parser::new(&text); let mut prql_blocks = vec![]; while let Some(event) = parser.next() { - match mdbook_prql::code_block_lang(&event) { - Some(lang) if lang.starts_with("prql") => { - let mut text = String::new(); - while let Some(Event::Text(line)) = parser.next() { - text.push_str(line.to_string().as_str()); - } - if text.is_empty() { - bail!("Expected text after PRQL code block"); - } - if lang == "prql" { - prql_blocks.push(text.to_string()); - } else if lang == "prql_error" { - prql_blocks.push(format!("# Error expected\n\n{text}")); - } else if lang == "prql_no_fmt" { - prql_blocks.push(format!("# Can't yet format & compile\n\n{text}")); - } + let Some(lang_tags) = code_block_lang_tags(&event) else { + continue + }; + + if lang_tags.contains(&"prql".to_string()) { + let mut prql_text = String::new(); + while let Some(Event::Text(line)) = parser.next() { + prql_text.push_str(line.to_string().as_str()); + } + if prql_text.is_empty() { + bail!("Expected text after PRQL code block"); + } + if lang_tags.contains(&"error".to_string()) { + prql_blocks.push(format!("# error\n\n{prql_text}")); + } else if lang_tags.contains(&"no-fmt".to_string()) { + prql_blocks.push(format!("# no-fmt\n\n{prql_text}")); + } else { + prql_blocks.push(prql_text.to_string()); } - _ => {} } } let snapshot_prefix = &dir_entry @@ -104,7 +110,7 @@ fn test_display() -> Result<(), ErrorMessages> { collect_book_examples()? .iter() .try_for_each(|(path, prql)| { - if prql.contains("# Error expected") || prql.contains("# Can't yet format & compile") { + if prql.contains("# error") || prql.contains("# no-fmt") { return Ok(()); } prql_to_pl(prql) @@ -114,7 +120,7 @@ fn test_display() -> Result<(), ErrorMessages> { panic!( " Failed compiling the formatted result of {path:?} -To skip this test for an example, use `prql_no_fmt` as the language label. +To skip this test for an example, use `prql,no-fmt` as the language label. The original PRQL was: @@ -135,7 +141,7 @@ The original PRQL was: #[test] fn test_rq_serialize() -> Result<(), ErrorMessages> { for (_, prql) in collect_book_examples()? { - if prql.contains("# Error expected") { + if prql.contains("# error") { continue; } let rq = prql_to_pl(&prql).map(pl_to_rq)?;