diff --git a/crates/mq-check/src/format.rs b/crates/mq-check/src/format.rs index 79ca0d067..9f4c55392 100644 --- a/crates/mq-check/src/format.rs +++ b/crates/mq-check/src/format.rs @@ -58,7 +58,7 @@ pub(crate) fn syntax_diagnostics(hir: &Hir, source_id: SourceId) -> Vec = hir .errors() .iter() - .filter(|error| hir_error_symbol(error).source.source_id == Some(source_id)) + .filter(|error| error.symbol().source.source_id == Some(source_id)) .map(|error| CheckDiagnostic { severity: Severity::Error, code: hir_error_code(error), @@ -88,19 +88,12 @@ fn hir_error_code(error: &HirError) -> &'static str { HirError::UnresolvedSymbol { .. } => "hir::unresolved_symbol", HirError::ModuleNotFound { .. } => "hir::module_not_found", HirError::YieldOutsideFunction { .. } => "hir::yield_outside_function", - } -} - -pub(crate) fn hir_error_symbol(error: &HirError) -> &Symbol { - match error { - HirError::UnresolvedSymbol { symbol, .. } - | HirError::ModuleNotFound { symbol, .. } - | HirError::YieldOutsideFunction { symbol } => symbol, + _ => "hir::unknown", } } pub(crate) fn hir_error_range(error: &HirError) -> mq_lang::Range { - hir_error_symbol(error).source.text_range.unwrap_or_default() + error.symbol().source.text_range.unwrap_or_default() } /// Returns a stable rule code for a [`HirWarning`] variant. @@ -149,6 +142,7 @@ fn type_error_code(error: &TypeError) -> &'static str { TypeError::NullablePropagation { .. } => "typechecker::nullable_propagation", TypeError::UnreachableCode { .. } => "typechecker::unreachable_code", TypeError::NonExhaustiveMatch { .. } => "typechecker::non_exhaustive_patterns", + _ => "typechecker::unknown", } } diff --git a/crates/mq-check/src/lib.rs b/crates/mq-check/src/lib.rs index ba47b32b2..60710aa68 100644 --- a/crates/mq-check/src/lib.rs +++ b/crates/mq-check/src/lib.rs @@ -88,6 +88,7 @@ impl<'a> IntoIterator for &'a TypeEnv { /// Type checking errors #[derive(Debug, Error, Clone, Diagnostic)] #[allow(unused_assignments)] +#[non_exhaustive] pub enum TypeError { #[error("Type mismatch: expected {expected}, found {found}")] #[diagnostic(code(typechecker::type_mismatch))] @@ -263,6 +264,7 @@ pub(crate) fn walk_ancestors( /// Options for configuring the type checker behavior #[derive(Debug, Clone, Default)] +#[non_exhaustive] pub struct TypeCheckerOptions { /// When true, arrays must contain elements of a single type. /// Heterogeneous arrays like `[1, "hello"]` will produce a type error. @@ -274,6 +276,26 @@ pub struct TypeCheckerOptions { pub input_type: Option, } +impl TypeCheckerOptions { + /// Sets `strict_array`. + pub fn with_strict_array(mut self, strict_array: bool) -> Self { + self.strict_array = strict_array; + self + } + + /// Sets `no_exhaustive_patterns`. + pub fn with_no_exhaustive_patterns(mut self, no_exhaustive_patterns: bool) -> Self { + self.no_exhaustive_patterns = no_exhaustive_patterns; + self + } + + /// Sets `input_type`. + pub fn with_input_type(mut self, input_type: Option) -> Self { + self.input_type = input_type; + self + } +} + /// Wall-clock time of each phase of [`TypeChecker::check_profiled`], in execution order. /// /// A phase that runs more than once appears once per run. diff --git a/crates/mq-check/src/main.rs b/crates/mq-check/src/main.rs index 1ea1edf2b..545f5a90a 100644 --- a/crates/mq-check/src/main.rs +++ b/crates/mq-check/src/main.rs @@ -48,11 +48,10 @@ struct Cli { impl Cli { fn type_checker_options(&self) -> TypeCheckerOptions { - TypeCheckerOptions { - strict_array: self.strict_array, - no_exhaustive_patterns: self.no_exhaustive_patterns, - input_type: self.input_type.clone(), - } + TypeCheckerOptions::default() + .with_strict_array(self.strict_array) + .with_no_exhaustive_patterns(self.no_exhaustive_patterns) + .with_input_type(self.input_type.clone()) } } @@ -252,7 +251,7 @@ fn check_syntax(w: &mut impl Write, hir: &mq_hir::Hir, source_id: mq_hir::Source let errors: Vec<(String, mq_lang::Range)> = hir .errors() .iter() - .filter(|error| format::hir_error_symbol(error).source.source_id == Some(source_id)) + .filter(|error| error.symbol().source.source_id == Some(source_id)) .map(|error| (error.to_string(), format::hir_error_range(error))) .collect(); let warnings: Vec<(String, mq_lang::Range)> = hir @@ -425,6 +424,7 @@ fn error_title(error: &TypeError) -> String { TypeError::NonExhaustiveMatch { missing, .. } => { format!("non-exhaustive patterns: missing case for {missing}") } + _ => error.to_string(), } } @@ -560,10 +560,8 @@ mod tests { #[test] fn test_input_type_is_applied_to_the_checked_source() { - let options = TypeCheckerOptions { - input_type: Some(mq_check::type_expr::parse_type("code").unwrap()), - ..Default::default() - }; + let options = + TypeCheckerOptions::default().with_input_type(Some(mq_check::type_expr::parse_type("code").unwrap())); let diagnostics = collect_check_diagnostics(".depth", None, false, &options); assert!(diagnostics.iter().any(|d| d.code == "typechecker::undefined_attribute")); let diagnostics = collect_check_diagnostics(".depth", None, false, &TypeCheckerOptions::default()); diff --git a/crates/mq-check/src/narrowing.rs b/crates/mq-check/src/narrowing.rs index 1916808e9..92d095f8e 100644 --- a/crates/mq-check/src/narrowing.rs +++ b/crates/mq-check/src/narrowing.rs @@ -150,7 +150,8 @@ pub(crate) fn selector_kinds(selector: &mq_lang::Selector) -> Option { Selector::MdxTextExpression => one(NodeKind::MdxTextExpression), Selector::MdxJsEsm => one(NodeKind::MdxJsEsm), Selector::MdxJsxFlowElement => one(NodeKind::MdxJsxFlowElement), - Selector::Recursive | Selector::Attr(_) | Selector::Property(_) => return None, + // `Selector` is `#[non_exhaustive]`: unknown selectors narrow nothing. + _ => return None, }) } diff --git a/crates/mq-check/tests/node_types_test.rs b/crates/mq-check/tests/node_types_test.rs index a04039043..63ed92a51 100644 --- a/crates/mq-check/tests/node_types_test.rs +++ b/crates/mq-check/tests/node_types_test.rs @@ -170,10 +170,10 @@ fn test_attribute_of_a_constructed_node_is_checked() { fn errors_with_input(code: &str, input_type: &str) -> Vec { let mut hir = Hir::default(); hir.add_code(None, code); - TypeChecker::with_options(mq_check::TypeCheckerOptions { - input_type: Some(mq_check::type_expr::parse_type(input_type).unwrap()), - ..Default::default() - }) + TypeChecker::with_options( + mq_check::TypeCheckerOptions::default() + .with_input_type(Some(mq_check::type_expr::parse_type(input_type).unwrap())), + ) .check(&hir) } diff --git a/crates/mq-check/tests/type_errors_test.rs b/crates/mq-check/tests/type_errors_test.rs index 46e0ef923..a13d3dcb0 100644 --- a/crates/mq-check/tests/type_errors_test.rs +++ b/crates/mq-check/tests/type_errors_test.rs @@ -419,20 +419,14 @@ fn test_narrowing_selector_various_structural() { /// Helper function to run type checker with strict array mode enabled fn check_types_strict_array(code: &str) -> Vec { let hir = create_hir(code); - let mut checker = TypeChecker::with_options(TypeCheckerOptions { - strict_array: true, - ..Default::default() - }); + let mut checker = TypeChecker::with_options(TypeCheckerOptions::default().with_strict_array(true)); checker.check(&hir) } /// Helper function to run type checker with tuple mode enabled fn check_types_tuple(code: &str) -> Vec { let hir = create_hir(code); - let mut checker = TypeChecker::with_options(TypeCheckerOptions { - strict_array: false, - ..Default::default() - }); + let mut checker = TypeChecker::with_options(TypeCheckerOptions::default().with_strict_array(false)); checker.check(&hir) } diff --git a/crates/mq-crawler/src/http_client.rs b/crates/mq-crawler/src/http_client.rs index e1f106072..779f56938 100644 --- a/crates/mq-crawler/src/http_client.rs +++ b/crates/mq-crawler/src/http_client.rs @@ -15,6 +15,7 @@ use url::Url; /// /// Retries on network errors, `429`, and `5xx`; other client errors (e.g. `404`) are not retried. #[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] pub struct RetryConfig { /// Retry attempts after the initial request fails. pub max_retries: u32, @@ -26,6 +27,32 @@ pub struct RetryConfig { pub backoff_multiplier: f64, } +impl RetryConfig { + /// Sets `max_retries`. + pub fn with_max_retries(mut self, max_retries: u32) -> Self { + self.max_retries = max_retries; + self + } + + /// Sets `initial_backoff`. + pub fn with_initial_backoff(mut self, initial_backoff: Duration) -> Self { + self.initial_backoff = initial_backoff; + self + } + + /// Sets `max_backoff`. + pub fn with_max_backoff(mut self, max_backoff: Duration) -> Self { + self.max_backoff = max_backoff; + self + } + + /// Sets `backoff_multiplier`. + pub fn with_backoff_multiplier(mut self, backoff_multiplier: f64) -> Self { + self.backoff_multiplier = backoff_multiplier; + self + } +} + impl Default for RetryConfig { fn default() -> Self { Self { @@ -54,6 +81,7 @@ impl RetryConfig { /// Default `Authorization` header credentials for a `reqwest`-based [`HttpClient`]. #[derive(Debug, Clone)] +#[non_exhaustive] pub enum AuthConfig { /// `Authorization: Basic `. Basic { username: String, password: Option }, @@ -81,6 +109,7 @@ impl FetchError { /// Multiple strategies can be combined; they are executed in order: /// network-idle / selector wait first, then the fixed delay. #[derive(Debug, Clone, Default)] +#[non_exhaustive] pub struct ChromiumWaitConfig { /// Optional fixed delay applied after all other strategies complete. pub fixed_delay: Duration, @@ -95,6 +124,32 @@ pub struct ChromiumWaitConfig { pub strategy_timeout: Duration, } +impl ChromiumWaitConfig { + /// Sets `fixed_delay`. + pub fn with_fixed_delay(mut self, fixed_delay: Duration) -> Self { + self.fixed_delay = fixed_delay; + self + } + + /// Sets `wait_for_selector`. + pub fn with_wait_for_selector(mut self, wait_for_selector: Option) -> Self { + self.wait_for_selector = wait_for_selector; + self + } + + /// Sets `network_idle`. + pub fn with_network_idle(mut self, network_idle: bool) -> Self { + self.network_idle = network_idle; + self + } + + /// Sets `strategy_timeout`. + pub fn with_strategy_timeout(mut self, strategy_timeout: Duration) -> Self { + self.strategy_timeout = strategy_timeout; + self + } +} + /// Enum for different HTTP client implementations #[derive(Debug, Clone)] pub enum HttpClient { diff --git a/crates/mq-crawler/src/main.rs b/crates/mq-crawler/src/main.rs index 16a42907a..dda671cac 100644 --- a/crates/mq-crawler/src/main.rs +++ b/crates/mq-crawler/src/main.rs @@ -226,12 +226,11 @@ async fn main() { v }); - let retry_config = mq_crawler::http_client::RetryConfig { - max_retries: args.max_retries, - initial_backoff: std::time::Duration::from_secs_f64(args.retry_initial_backoff.max(0.0)), - max_backoff: std::time::Duration::from_secs_f64(args.retry_max_backoff.max(0.0)), - backoff_multiplier: args.retry_backoff_multiplier, - }; + let retry_config = mq_crawler::http_client::RetryConfig::default() + .with_max_retries(args.max_retries) + .with_initial_backoff(std::time::Duration::from_secs_f64(args.retry_initial_backoff.max(0.0))) + .with_max_backoff(std::time::Duration::from_secs_f64(args.retry_max_backoff.max(0.0))) + .with_backoff_multiplier(args.retry_backoff_multiplier); let header_map = match build_header_map(&args.headers, &args.cookie) { Ok(header_map) => header_map, @@ -309,12 +308,11 @@ async fn main() { std::time::Duration::from_secs_f64(headless_wait_secs) }; - let wait_config = mq_crawler::http_client::ChromiumWaitConfig { - fixed_delay, - wait_for_selector: args.headless_wait_for_selector.clone(), - network_idle: args.headless_network_idle, - strategy_timeout, - }; + let wait_config = mq_crawler::http_client::ChromiumWaitConfig::default() + .with_fixed_delay(fixed_delay) + .with_wait_for_selector(args.headless_wait_for_selector.clone()) + .with_network_idle(args.headless_network_idle) + .with_strategy_timeout(strategy_timeout); mq_crawler::http_client::HttpClient::new_chromium(args.chrome_path, wait_config, retry_config.clone()) .await @@ -381,12 +379,10 @@ async fn main() { args.output, args.concurrency, format, - mq_markdown::ConversionOptions { - extract_scripts_as_code_blocks: args.conversion.extract_scripts_as_code_blocks, - generate_front_matter: args.conversion.generate_front_matter, - use_title_as_h1: args.conversion.use_title_as_h1, - base_url: None, - }, + mq_markdown::ConversionOptions::default() + .with_extract_scripts_as_code_blocks(args.conversion.extract_scripts_as_code_blocks) + .with_generate_front_matter(args.conversion.generate_front_matter) + .with_use_title_as_h1(args.conversion.use_title_as_h1), args.depth, effective_allowed, sitemap_seed_urls, diff --git a/crates/mq-dap/src/error.rs b/crates/mq-dap/src/error.rs index 6fd5954a5..9046b7825 100644 --- a/crates/mq-dap/src/error.rs +++ b/crates/mq-dap/src/error.rs @@ -4,6 +4,7 @@ use dap::prelude::Command; use thiserror::Error; #[derive(Error, Debug)] +#[non_exhaustive] pub enum MqAdapterError { #[error("Unhandled command: {0:?}")] UnhandledCommand(Box), diff --git a/crates/mq-ffi/src/lib.rs b/crates/mq-ffi/src/lib.rs index 77939ac65..9f7526c1f 100644 --- a/crates/mq-ffi/src/lib.rs +++ b/crates/mq-ffi/src/lib.rs @@ -109,12 +109,11 @@ impl From for ConversionOptions { .filter(|s| !s.is_empty()) .map(|s| s.to_string()) }; - ConversionOptions { - extract_scripts_as_code_blocks: options.extract_scripts_as_code_blocks, - generate_front_matter: options.generate_front_matter, - use_title_as_h1: options.use_title_as_h1, - base_url, - } + ConversionOptions::default() + .with_extract_scripts_as_code_blocks(options.extract_scripts_as_code_blocks) + .with_generate_front_matter(options.generate_front_matter) + .with_use_title_as_h1(options.use_title_as_h1) + .with_base_url(base_url) } } diff --git a/crates/mq-formatter/benches/benchmark.rs b/crates/mq-formatter/benches/benchmark.rs index 4296e1c8b..12780d860 100644 --- a/crates/mq-formatter/benches/benchmark.rs +++ b/crates/mq-formatter/benches/benchmark.rs @@ -46,13 +46,10 @@ def y(): test; | let a = 2 | import "a.mq" def b(): test;"#; - let config = FormatterConfig { - indent_width: 2, - sort_imports: true, - sort_functions: true, - sort_fields: true, - max_width: None, - }; + let config = FormatterConfig::default() + .with_sort_imports(true) + .with_sort_functions(true) + .with_sort_fields(true); let mut formatter = Formatter::new(Some(config)); formatter.format(code).unwrap(); } diff --git a/crates/mq-formatter/src/formatter.rs b/crates/mq-formatter/src/formatter.rs index dbf4a43fb..bb21da1bb 100644 --- a/crates/mq-formatter/src/formatter.rs +++ b/crates/mq-formatter/src/formatter.rs @@ -59,6 +59,7 @@ enum SortPriority { } #[derive(Clone, Debug)] +#[non_exhaustive] pub struct FormatterConfig { pub indent_width: usize, pub sort_imports: bool, @@ -67,6 +68,38 @@ pub struct FormatterConfig { pub max_width: Option, } +impl FormatterConfig { + /// Sets `indent_width`. + pub fn with_indent_width(mut self, indent_width: usize) -> Self { + self.indent_width = indent_width; + self + } + + /// Sets `sort_imports`. + pub fn with_sort_imports(mut self, sort_imports: bool) -> Self { + self.sort_imports = sort_imports; + self + } + + /// Sets `sort_functions`. + pub fn with_sort_functions(mut self, sort_functions: bool) -> Self { + self.sort_functions = sort_functions; + self + } + + /// Sets `sort_fields`. + pub fn with_sort_fields(mut self, sort_fields: bool) -> Self { + self.sort_fields = sort_fields; + self + } + + /// Sets `max_width`. + pub fn with_max_width(mut self, max_width: Option) -> Self { + self.max_width = max_width; + self + } +} + impl Default for FormatterConfig { fn default() -> Self { Self { diff --git a/crates/mq-formatter/src/main.rs b/crates/mq-formatter/src/main.rs index 4a3838d8e..6fec58e88 100644 --- a/crates/mq-formatter/src/main.rs +++ b/crates/mq-formatter/src/main.rs @@ -52,13 +52,14 @@ struct Cli { fn main() -> miette::Result<()> { let cli = Cli::parse(); - let mut formatter = mq_formatter::Formatter::new(Some(mq_formatter::FormatterConfig { - indent_width: cli.indent_width, - sort_imports: cli.sort_imports, - sort_fields: cli.sort_fields, - sort_functions: cli.sort_functions, - max_width: cli.max_width, - })); + let mut formatter = mq_formatter::Formatter::new(Some( + mq_formatter::FormatterConfig::default() + .with_indent_width(cli.indent_width) + .with_sort_imports(cli.sort_imports) + .with_sort_fields(cli.sort_fields) + .with_sort_functions(cli.sort_functions) + .with_max_width(cli.max_width), + )); if let Some(files) = &cli.files && files.len() == 1 diff --git a/crates/mq-formatter/tests/integration_tests.rs b/crates/mq-formatter/tests/integration_tests.rs index 8f4ce1e4d..b4ddbc9b3 100644 --- a/crates/mq-formatter/tests/integration_tests.rs +++ b/crates/mq-formatter/tests/integration_tests.rs @@ -52,10 +52,7 @@ fn test_bundled_mq_files_format_idempotently() { assert!(paths.len() > 2, "no bundled .mq files found under {root}"); for max_width in [None, Some(60)] { - let config = mq_formatter::FormatterConfig { - max_width, - ..Default::default() - }; + let config = mq_formatter::FormatterConfig::default().with_max_width(max_width); for path in &paths { let code = std::fs::read_to_string(path).unwrap(); let formatted = mq_formatter::Formatter::new(Some(config.clone())) diff --git a/crates/mq-hir/src/error.rs b/crates/mq-hir/src/error.rs index 2108d81de..ff2adcbb8 100644 --- a/crates/mq-hir/src/error.rs +++ b/crates/mq-hir/src/error.rs @@ -4,6 +4,7 @@ use thiserror::Error; use crate::{Hir, ScopeId, ScopeKind, Symbol, SymbolKind}; #[derive(Debug, Error)] +#[non_exhaustive] pub enum HirError { #[error( "Unresolved symbol: {}", @@ -22,6 +23,17 @@ pub enum HirError { YieldOutsideFunction { symbol: Symbol }, } +impl HirError { + /// The symbol the error is reported on. + pub fn symbol(&self) -> &Symbol { + match self { + HirError::UnresolvedSymbol { symbol, .. } + | HirError::ModuleNotFound { symbol, .. } + | HirError::YieldOutsideFunction { symbol } => symbol, + } + } +} + #[derive(Debug, Error)] pub enum HirWarning { #[error("Unreachable code after halt() function call")] diff --git a/crates/mq-lang/src/ast/node.rs b/crates/mq-lang/src/ast/node.rs index ba0135624..a46e5264c 100644 --- a/crates/mq-lang/src/ast/node.rs +++ b/crates/mq-lang/src/ast/node.rs @@ -14,6 +14,7 @@ use std::{ /// Represents a function parameter with an optional default value #[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))] #[derive(PartialEq, PartialOrd, Debug, Clone)] +#[non_exhaustive] pub struct Param { pub ident: IdentWithToken, pub default: Option>, @@ -210,6 +211,7 @@ impl Node { #[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))] #[derive(PartialEq, Debug, Eq, Clone)] +#[non_exhaustive] pub struct IdentWithToken { pub name: Ident, #[cfg_attr(feature = "ast-json", serde(skip_serializing, skip_deserializing, default))] @@ -264,6 +266,7 @@ pub enum StringSegment { #[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))] #[derive(PartialEq, PartialOrd, Debug, Clone)] +#[non_exhaustive] pub enum Pattern { Literal(Literal), Ident(IdentWithToken), @@ -277,6 +280,7 @@ pub enum Pattern { #[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))] #[derive(PartialEq, PartialOrd, Debug, Clone)] +#[non_exhaustive] pub struct MatchArm { pub pattern: Pattern, pub guard: Option>, @@ -285,6 +289,7 @@ pub struct MatchArm { #[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))] #[derive(PartialEq, PartialOrd, Debug, Clone)] +#[non_exhaustive] pub enum Literal { String(String), Bytes(Vec), @@ -302,6 +307,7 @@ impl From<&str> for Literal { #[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))] #[derive(PartialEq, PartialOrd, Debug, Clone)] +#[non_exhaustive] pub enum AccessTarget { Call(IdentWithToken, Box), Ident(IdentWithToken), @@ -334,6 +340,7 @@ impl Display for Literal { /// Compiles directly to a VM opcode; see `Expr::BinaryOp`. #[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))] #[derive(PartialEq, PartialOrd, Debug, Clone, Copy)] +#[non_exhaustive] pub enum BinaryOp { Add, Sub, @@ -375,6 +382,7 @@ impl Display for BinaryOp { /// A prefix operator (`!` or `-`). Compiles directly to a VM opcode; see `Expr::UnaryOp`. #[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))] #[derive(PartialEq, PartialOrd, Debug, Clone, Copy)] +#[non_exhaustive] pub enum UnaryOp { Not, Neg, @@ -397,6 +405,7 @@ impl Display for UnaryOp { #[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))] #[derive(PartialEq, PartialOrd, Debug, Clone)] +#[non_exhaustive] pub enum Expr { As(IdentWithToken, Shared), Block(Program), diff --git a/crates/mq-lang/src/cst/error.rs b/crates/mq-lang/src/cst/error.rs index 598438e87..48ea4569d 100644 --- a/crates/mq-lang/src/cst/error.rs +++ b/crates/mq-lang/src/cst/error.rs @@ -3,6 +3,7 @@ use thiserror::Error; use crate::{Shared, Token, TokenKind, selector}; #[derive(Error, Debug, PartialEq, Clone, PartialOrd, Eq, Ord)] +#[non_exhaustive] pub enum ParseError { #[error("Unexpected token `{0}`")] UnexpectedToken(Shared), diff --git a/crates/mq-lang/src/engine.rs b/crates/mq-lang/src/engine.rs index eaaf719f0..78a489db8 100644 --- a/crates/mq-lang/src/engine.rs +++ b/crates/mq-lang/src/engine.rs @@ -30,6 +30,7 @@ use crate::{ /// An error returned when a value cannot be added to an [`Engine`] environment. #[derive(Debug, thiserror::Error, PartialEq, Eq)] +#[non_exhaustive] pub enum DefineValueError { /// The value contains execution state that belongs to a particular VM. #[error("cannot define a VM-bound {0}")] diff --git a/crates/mq-lang/src/error.rs b/crates/mq-lang/src/error.rs index 3462563cc..dcbd75ad9 100644 --- a/crates/mq-lang/src/error.rs +++ b/crates/mq-lang/src/error.rs @@ -13,6 +13,7 @@ use crate::{ #[allow(clippy::useless_conversion)] #[derive(Debug, thiserror::Error, PartialEq)] +#[non_exhaustive] pub enum InnerError { #[error(transparent)] Runtime(#[from] RuntimeError), @@ -54,6 +55,7 @@ impl InnerError { /// Represents a high-level error with diagnostic information for the user. #[derive(PartialEq, Debug, thiserror::Error)] #[error("{cause}")] +#[non_exhaustive] pub struct Error { /// The underlying cause of the error. pub cause: InnerError, diff --git a/crates/mq-lang/src/error/runtime.rs b/crates/mq-lang/src/error/runtime.rs index 111f81658..4bdc52773 100644 --- a/crates/mq-lang/src/error/runtime.rs +++ b/crates/mq-lang/src/error/runtime.rs @@ -12,6 +12,7 @@ type ArgType = Vec; type ErrorToken = Token; #[derive(Error, Debug, PartialEq)] +#[non_exhaustive] pub enum RuntimeError { /// An uncaught runtime error annotated with the VM frames active at the failure point. #[error("{source}\n{trace}")] diff --git a/crates/mq-lang/src/error/syntax.rs b/crates/mq-lang/src/error/syntax.rs index 668ab8574..e3a853b72 100644 --- a/crates/mq-lang/src/error/syntax.rs +++ b/crates/mq-lang/src/error/syntax.rs @@ -5,6 +5,7 @@ use crate::{Token, module::ModuleId, selector}; /// Errors that occur during parsing of mq source code. #[derive(Error, Debug, PartialEq)] +#[non_exhaustive] pub enum SyntaxError { /// An environment variable was not found. #[error("Undefined environment variable `{1}`")] diff --git a/crates/mq-lang/src/io/error.rs b/crates/mq-lang/src/io/error.rs index 61a4ec65e..66f0af542 100644 --- a/crates/mq-lang/src/io/error.rs +++ b/crates/mq-lang/src/io/error.rs @@ -6,6 +6,7 @@ use thiserror::Error; /// already use (e.g. `ModuleError` for module resolution, the builtin /// evaluator's own error type) rather than surface it to `miette` directly. #[derive(Debug, Clone, PartialEq, Error)] +#[non_exhaustive] pub enum IoError { #[error("{0} not found")] NotFound(Cow<'static, str>), diff --git a/crates/mq-lang/src/lexer.rs b/crates/mq-lang/src/lexer.rs index 8a8797de7..2dbcd011d 100644 --- a/crates/mq-lang/src/lexer.rs +++ b/crates/mq-lang/src/lexer.rs @@ -9,6 +9,7 @@ use crate::number::Number; use crate::range::{Position, Range}; #[derive(Debug, Clone, Default)] +#[non_exhaustive] pub struct Options { pub ignore_errors: bool, pub include_spaces: bool, diff --git a/crates/mq-lang/src/lib.rs b/crates/mq-lang/src/lib.rs index f2c89d382..27348b5ee 100644 --- a/crates/mq-lang/src/lib.rs +++ b/crates/mq-lang/src/lib.rs @@ -478,12 +478,10 @@ mod tests { "#; let result = parse_html_input_with_options( input, - mq_markdown::ConversionOptions { - extract_scripts_as_code_blocks: true, - generate_front_matter: true, - use_title_as_h1: true, - base_url: None, - }, + mq_markdown::ConversionOptions::default() + .with_extract_scripts_as_code_blocks(true) + .with_generate_front_matter(true) + .with_use_title_as_h1(true), ); assert!(result.is_ok()); assert_eq!( diff --git a/crates/mq-lang/src/module/error.rs b/crates/mq-lang/src/module/error.rs index d551f5af2..e2559a2c3 100644 --- a/crates/mq-lang/src/module/error.rs +++ b/crates/mq-lang/src/module/error.rs @@ -6,6 +6,7 @@ use std::borrow::Cow; /// Errors that can occur while loading or resolving mq modules. #[derive(Debug, PartialEq, Error)] +#[non_exhaustive] pub enum ModuleError { #[error("Module `{0}` is already loaded")] AlreadyLoaded(Cow<'static, str>), diff --git a/crates/mq-lang/src/mqc.rs b/crates/mq-lang/src/mqc.rs index 8caabb82f..cf5404e21 100644 --- a/crates/mq-lang/src/mqc.rs +++ b/crates/mq-lang/src/mqc.rs @@ -56,6 +56,7 @@ const RECOMPILE_HELP: &str = "Recompile the program from its source with this ve /// An error from compiling to or loading a `.mqc` file. #[derive(Debug, thiserror::Error, miette::Diagnostic)] +#[non_exhaustive] pub enum MqcError { #[error("not an mq bytecode file")] #[diagnostic(code(mq::mqc::not_mqc))] diff --git a/crates/mq-lang/src/runtime/runtime_value.rs b/crates/mq-lang/src/runtime/runtime_value.rs index 44e579eb9..9406ed4ff 100644 --- a/crates/mq-lang/src/runtime/runtime_value.rs +++ b/crates/mq-lang/src/runtime/runtime_value.rs @@ -60,6 +60,7 @@ pub enum ResumeBuiltin { /// program execution, including numbers, strings, markdown nodes, functions, /// and more complex data structures. #[derive(Clone, Default)] +#[non_exhaustive] pub enum RuntimeValue { /// A numeric value. Number(Number), diff --git a/crates/mq-lang/src/selector.rs b/crates/mq-lang/src/selector.rs index 1ab971401..548caa48c 100644 --- a/crates/mq-lang/src/selector.rs +++ b/crates/mq-lang/src/selector.rs @@ -91,6 +91,7 @@ fn escape_property_key(key: &str) -> String { /// for HTML. Each variant matches a specific type of markdown element. #[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))] #[derive(PartialEq, PartialOrd, Debug, Eq, Clone)] +#[non_exhaustive] pub enum Selector { /// Matches blockquote elements (e.g., `> quoted text`). Blockquote, diff --git a/crates/mq-lint/src/config.rs b/crates/mq-lint/src/config.rs index e39e07302..1a2a7d697 100644 --- a/crates/mq-lint/src/config.rs +++ b/crates/mq-lint/src/config.rs @@ -6,6 +6,7 @@ use crate::RuleId; /// Per-rule enable/disable flag. #[derive(Debug, Clone)] +#[non_exhaustive] pub struct RuleConfig { pub enabled: bool, } @@ -45,6 +46,7 @@ impl Default for ComplexityThresholds { /// Top-level linter configuration. #[derive(Debug, Clone, Default)] +#[non_exhaustive] pub struct LintConfig { /// Per-rule overrides. Rules not listed here use their default enabled state. pub rules: HashMap, diff --git a/crates/mq-lsp/src/error.rs b/crates/mq-lsp/src/error.rs index c6e52c02a..e76d9da11 100644 --- a/crates/mq-lsp/src/error.rs +++ b/crates/mq-lsp/src/error.rs @@ -2,6 +2,7 @@ use tower_lsp_server::ls_types; use tower_lsp_server::ls_types::NumberOrString; #[derive(Debug, Clone)] +#[non_exhaustive] pub enum LspError { SyntaxError(mq_lang::Diagnostic), TypeError(mq_check::TypeError), diff --git a/crates/mq-lsp/src/main.rs b/crates/mq-lsp/src/main.rs index 179f8212f..6f02cf830 100644 --- a/crates/mq-lsp/src/main.rs +++ b/crates/mq-lsp/src/main.rs @@ -78,11 +78,9 @@ fn parse_input_type(src: &str) -> Result { #[tokio::main] async fn main() { let cli = Cli::parse(); - let type_check_config = mq_check::TypeCheckerOptions { - strict_array: cli.type_check.strict_array, - input_type: cli.type_check.input_type.clone(), - ..Default::default() - }; + let type_check_config = mq_check::TypeCheckerOptions::default() + .with_strict_array(cli.type_check.strict_array) + .with_input_type(cli.type_check.input_type.clone()); let mut lint_config = mq_lint::LintConfig::default(); for rule_id in &cli.lint.disable_lint_rule { diff --git a/crates/mq-lsp/src/refactor.rs b/crates/mq-lsp/src/refactor.rs index 4082ec53f..50d72194f 100644 --- a/crates/mq-lsp/src/refactor.rs +++ b/crates/mq-lsp/src/refactor.rs @@ -214,12 +214,9 @@ fn deletion_range(container: &[Shared], idx: usize) -> MqRange { } fn format_snippet(code: &str) -> String { - mq_formatter::Formatter::new(Some(mq_formatter::FormatterConfig { - indent_width: 2, - ..Default::default() - })) - .format(code) - .unwrap_or_else(|_| code.to_string()) + mq_formatter::Formatter::new(Some(mq_formatter::FormatterConfig::default().with_indent_width(2))) + .format(code) + .unwrap_or_else(|_| code.to_string()) } fn single_edit_action( diff --git a/crates/mq-lsp/src/server.rs b/crates/mq-lsp/src/server.rs index 62b6db0a8..242aaa3f5 100644 --- a/crates/mq-lsp/src/server.rs +++ b/crates/mq-lsp/src/server.rs @@ -361,10 +361,8 @@ impl LanguageServer for Backend { // Replace the whole original text, not the formatted length. let end = document_end_position(&text); let formatted_text = tokio::task::spawn_blocking(move || { - let mut formatter = mq_formatter::Formatter::new(Some(mq_formatter::FormatterConfig { - indent_width: 2, - ..Default::default() - })); + let mut formatter = + mq_formatter::Formatter::new(Some(mq_formatter::FormatterConfig::default().with_indent_width(2))); match nodes { Some(nodes) => formatter.format_with_cst(&mut Vec::clone(&nodes)), None => formatter.format(&text), @@ -454,11 +452,8 @@ impl LanguageServer for Backend { } let formatted_range = tokio::task::spawn_blocking(move || { - mq_formatter::Formatter::new(Some(mq_formatter::FormatterConfig { - indent_width: 2, - ..Default::default() - })) - .format(&selected) + mq_formatter::Formatter::new(Some(mq_formatter::FormatterConfig::default().with_indent_width(2))) + .format(&selected) }) .await .map_err(|_| jsonrpc::Error::new(jsonrpc::ErrorCode::InternalError))? @@ -632,6 +627,7 @@ impl Backend { } #[derive(Debug, Clone, Default)] +#[non_exhaustive] pub struct LspConfig { module_paths: Vec, enable_type_checking: bool, @@ -813,12 +809,10 @@ mod tests { .await .unwrap() .unwrap(); - let expected = mq_formatter::Formatter::new(Some(mq_formatter::FormatterConfig { - indent_width: 2, - ..Default::default() - })) - .format(text) - .unwrap(); + let expected = + mq_formatter::Formatter::new(Some(mq_formatter::FormatterConfig::default().with_indent_width(2))) + .format(text) + .unwrap(); assert_eq!(edits[0].new_text, expected); assert_eq!(edits[0].range.end, document_end_position(text)); @@ -2494,10 +2488,7 @@ mod tests { let config = LspConfig::new( vec![], true, - mq_check::TypeCheckerOptions { - strict_array: true, - ..Default::default() - }, + mq_check::TypeCheckerOptions::default().with_strict_array(true), true, lint_config, ); @@ -2513,10 +2504,7 @@ mod tests { let config = LspConfig::new( vec![], true, - mq_check::TypeCheckerOptions { - input_type: Some(input_type.clone()), - ..Default::default() - }, + mq_check::TypeCheckerOptions::default().with_input_type(Some(input_type.clone())), false, mq_lint::LintConfig::default(), ); @@ -2733,10 +2721,7 @@ mod tests { config: LspConfig::new( vec![], true, - mq_check::TypeCheckerOptions { - strict_array: true, - ..Default::default() - }, + mq_check::TypeCheckerOptions::default().with_strict_array(true), false, mq_lint::LintConfig::default(), ), @@ -2784,10 +2769,7 @@ mod tests { config: LspConfig::new( vec![], true, - mq_check::TypeCheckerOptions { - strict_array: false, - ..Default::default() - }, + mq_check::TypeCheckerOptions::default().with_strict_array(false), false, mq_lint::LintConfig::default(), ), diff --git a/crates/mq-markdown/examples/html_convert.rs b/crates/mq-markdown/examples/html_convert.rs index 98e465c79..15ee1db4b 100644 --- a/crates/mq-markdown/examples/html_convert.rs +++ b/crates/mq-markdown/examples/html_convert.rs @@ -15,12 +15,7 @@ fn main() { std::process::exit(1); }); - let options = ConversionOptions { - extract_scripts_as_code_blocks: false, - generate_front_matter: false, - use_title_as_h1: false, - base_url: None, - }; + let options = ConversionOptions::default(); match convert_html_to_markdown(&html, options) { Ok(md) => print!("{}", md), diff --git a/crates/mq-markdown/src/html_to_markdown/options.rs b/crates/mq-markdown/src/html_to_markdown/options.rs index a293cdd6c..5ec20d9bb 100644 --- a/crates/mq-markdown/src/html_to_markdown/options.rs +++ b/crates/mq-markdown/src/html_to_markdown/options.rs @@ -1,4 +1,5 @@ #[derive(Debug, Clone, Default)] +#[non_exhaustive] pub struct ConversionOptions { pub extract_scripts_as_code_blocks: bool, pub generate_front_matter: bool, @@ -6,3 +7,29 @@ pub struct ConversionOptions { /// Base URL for resolving relative `href`/`src` values; falls back to ``. pub base_url: Option, } + +impl ConversionOptions { + /// Sets `extract_scripts_as_code_blocks`. + pub fn with_extract_scripts_as_code_blocks(mut self, extract_scripts_as_code_blocks: bool) -> Self { + self.extract_scripts_as_code_blocks = extract_scripts_as_code_blocks; + self + } + + /// Sets `generate_front_matter`. + pub fn with_generate_front_matter(mut self, generate_front_matter: bool) -> Self { + self.generate_front_matter = generate_front_matter; + self + } + + /// Sets `use_title_as_h1`. + pub fn with_use_title_as_h1(mut self, use_title_as_h1: bool) -> Self { + self.use_title_as_h1 = use_title_as_h1; + self + } + + /// Sets `base_url`. + pub fn with_base_url(mut self, base_url: Option) -> Self { + self.base_url = base_url; + self + } +} diff --git a/crates/mq-markdown/src/lib.rs b/crates/mq-markdown/src/lib.rs index cf2963cfe..2aada2b3d 100644 --- a/crates/mq-markdown/src/lib.rs +++ b/crates/mq-markdown/src/lib.rs @@ -43,10 +43,7 @@ //! use mq_markdown::{Markdown, RenderOptions, ListStyle}; //! //! let mut doc = "- Item 1\n- Item 2".parse::().unwrap(); -//! doc.set_options(RenderOptions { -//! list_style: ListStyle::Plus, -//! ..Default::default() -//! }); +//! doc.set_options(RenderOptions::default().with_list_style(ListStyle::Plus)); //! //! // Now renders with "+" instead of "-" //! println!("{}", doc); diff --git a/crates/mq-markdown/src/node.rs b/crates/mq-markdown/src/node.rs index 48871b594..375f62a20 100644 --- a/crates/mq-markdown/src/node.rs +++ b/crates/mq-markdown/src/node.rs @@ -153,12 +153,33 @@ pub const EMPTY_NODE: Node = Node::Text(Text { }); #[derive(Debug, Clone, Default, PartialEq)] +#[non_exhaustive] pub struct RenderOptions { pub list_style: ListStyle, pub link_url_style: UrlSurroundStyle, pub link_title_style: TitleSurroundStyle, } +impl RenderOptions { + /// Sets `list_style`. + pub fn with_list_style(mut self, list_style: ListStyle) -> Self { + self.list_style = list_style; + self + } + + /// Sets `link_url_style`. + pub fn with_link_url_style(mut self, link_url_style: UrlSurroundStyle) -> Self { + self.link_url_style = link_url_style; + self + } + + /// Sets `link_title_style`. + pub fn with_link_title_style(mut self, link_title_style: TitleSurroundStyle) -> Self { + self.link_title_style = link_title_style; + self + } +} + #[derive(Debug, Clone, Default, PartialEq)] pub enum ListStyle { #[default] @@ -785,12 +806,17 @@ pub struct HorizontalRule { pub position: Option, } +/// A Markdown node. +/// +/// Some variants exist only when their feature (`callout`, `embed`, `wikilink`) is enabled, +/// and more may be added. Match with a wildcard arm. #[derive(Debug, Clone, Default)] #[cfg_attr( feature = "json", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase", untagged) )] +#[non_exhaustive] pub enum Node { Blockquote(Blockquote), Break(Break), diff --git a/crates/mq-markdown/tests/html_to_markdown_property_tests.rs b/crates/mq-markdown/tests/html_to_markdown_property_tests.rs index 790feff4e..99c5b8b99 100644 --- a/crates/mq-markdown/tests/html_to_markdown_property_tests.rs +++ b/crates/mq-markdown/tests/html_to_markdown_property_tests.rs @@ -82,10 +82,7 @@ proptest! { let html = format!( r#"

Body

"# ); - let options = ConversionOptions { - generate_front_matter: true, - ..Default::default() - }; + let options = ConversionOptions::default().with_generate_front_matter(true); let md = convert_html_to_markdown(&html, options).unwrap(); prop_assert!(md.contains(&desc), "description missing from front matter:\n{md}"); } diff --git a/crates/mq-markdown/tests/html_to_markdown_tests.rs b/crates/mq-markdown/tests/html_to_markdown_tests.rs index 2817c84c1..f148cf12c 100644 --- a/crates/mq-markdown/tests/html_to_markdown_tests.rs +++ b/crates/mq-markdown/tests/html_to_markdown_tests.rs @@ -37,82 +37,52 @@ fn assert_conversion_with_options(html: &str, expected_markdown: &str, options: )] #[case::script_tag_inline_javascript_default_type( "", - ConversionOptions { - extract_scripts_as_code_blocks: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_extract_scripts_as_code_blocks(true), "```\nalert('Hello');\n```", )] #[case::script_tag_inline_javascript_text_javascript( "", - ConversionOptions{ - extract_scripts_as_code_blocks: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_extract_scripts_as_code_blocks(true), "```javascript\nconsole.log(1);\n```" )] #[case::script_tag_inline_javascript_application_javascript( "", - ConversionOptions{ - extract_scripts_as_code_blocks: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_extract_scripts_as_code_blocks(true), "```javascript\nlet a = 1;\n```" )] #[case::script_tag_inline_javascript_module( "", - ConversionOptions{ - extract_scripts_as_code_blocks: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_extract_scripts_as_code_blocks(true), "```javascript\nimport { B } from './mod.js';\n```" )] #[case::script_tag_inline_json_ld( "", - ConversionOptions { - extract_scripts_as_code_blocks: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_extract_scripts_as_code_blocks(true), "```json\n{\"@context\":\"schema.org\"}\n```" )] #[case::script_tag_inline_json( "", - ConversionOptions { - extract_scripts_as_code_blocks: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_extract_scripts_as_code_blocks(true), "```json\n{\"key\":\"value\"}\n```" )] #[case::script_tag_unknown_type( "", - ConversionOptions { - extract_scripts_as_code_blocks: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_extract_scripts_as_code_blocks(true), "```\ncontent\n```" )] #[case::script_tag_empty_content( "", - ConversionOptions { - extract_scripts_as_code_blocks: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_extract_scripts_as_code_blocks(true), "```\n\n```" )] #[case::script_tag_external_src_ignored_when_option_true( "", - ConversionOptions { - extract_scripts_as_code_blocks: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_extract_scripts_as_code_blocks(true), "" )] #[case::script_tag_with_html_comments_and_cdata( "", - ConversionOptions { - extract_scripts_as_code_blocks: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_extract_scripts_as_code_blocks(true), &format!("```\n{}\n```", "") )] #[case::front_matter_disabled( @@ -122,171 +92,107 @@ fn assert_conversion_with_options(html: &str, expected_markdown: &str, options: )] #[case::front_matter_title_only( "My Title

Body

", - ConversionOptions { - generate_front_matter: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_generate_front_matter(true), "---\ntitle: My Title\n---\n\nBody", )] #[case::front_matter_description( "

B

", - ConversionOptions { - generate_front_matter: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_generate_front_matter(true), "---\ndescription: Page description.\n---\n\nB", )] #[case::front_matter_keywords_single( "

B

", - ConversionOptions { - generate_front_matter: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_generate_front_matter(true), "---\nkeywords:\n - rust\n---\n\nB", )] #[case::front_matter_keywords_multiple_comma_separated( "

B

", - ConversionOptions { - generate_front_matter: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_generate_front_matter(true), "---\nkeywords:\n - rust\n - web\n - html\n---\n\nB", )] #[case::front_matter_keywords_comma_space_separated( "

B

", - ConversionOptions { - generate_front_matter: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_generate_front_matter(true), "---\nkeywords:\n - rust\n - web\n - html\n---\n\nB", )] #[case::front_matter_author( "

B

", - ConversionOptions { - generate_front_matter: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_generate_front_matter(true), "---\nauthor: Jules Verne\n---\n\nB", )] #[case::front_matter_all_present( "Full Test

Content

", - ConversionOptions { - generate_front_matter: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_generate_front_matter(true), "---\nauthor: Author Name\ndescription: Desc here\nkeywords:\n - key1\n - key2\ntitle: Full Test\n---\n\nContent", )] #[case::front_matter_no_head_tag( "

Only body

", - ConversionOptions { - generate_front_matter: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_generate_front_matter(true), "Only body", )] #[case::front_matter_empty_head( "

Body

", - ConversionOptions { - generate_front_matter: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_generate_front_matter(true), "Body", )] #[case::front_matter_no_relevant_tags_in_head( "

Body

", - ConversionOptions { - generate_front_matter: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_generate_front_matter(true), "Body", // No relevant tags for front matter )] #[case::front_matter_with_script_extraction_option( "Script Page

Text

", - ConversionOptions { - generate_front_matter: true, - extract_scripts_as_code_blocks: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_generate_front_matter(true).with_extract_scripts_as_code_blocks(true), "---\ntitle: Script Page\n---\n\n```\nlet x=1;\n```\n\nText", )] #[case::front_matter_html_fragment_no_head( "

Just a paragraph

", - ConversionOptions { - generate_front_matter: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_generate_front_matter(true), "Just a paragraph", // Meta tag not in context )] #[case::front_matter_og_description_fallback( "

B

", - ConversionOptions { - generate_front_matter: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_generate_front_matter(true), "---\ndescription: OGP desc\n---\n\nB", )] #[case::front_matter_name_description_wins_over_og( "

B

", - ConversionOptions { - generate_front_matter: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_generate_front_matter(true), "---\ndescription: Named desc\n---\n\nB", )] #[case::front_matter_schema_author_wins_over_twitter_handle( r#"

B

"#, - ConversionOptions { - generate_front_matter: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_generate_front_matter(true), "---\nauthor: Jules Verne\n---\n\nB", )] #[case::front_matter_twitter_creator_fallback( "

B

", - ConversionOptions { - generate_front_matter: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_generate_front_matter(true), "---\nauthor: \"@jverne\"\n---\n\nB", )] #[case::front_matter_article_published_time( "

B

", - ConversionOptions { - generate_front_matter: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_generate_front_matter(true), "---\npublished: \"2024-01-02T00:00:00Z\"\n---\n\nB", )] #[case::front_matter_og_image_and_site_name( "

B

", - ConversionOptions { - generate_front_matter: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_generate_front_matter(true), "---\nimage: \"https://example.com/a.png\"\nsite: Example Site\n---\n\nB", )] #[case::front_matter_multibyte_description_and_author( "

本文

", - ConversionOptions { - generate_front_matter: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_generate_front_matter(true), "---\nauthor: 山田太郎\ndescription: 日本語の説明文です\n---\n\n本文", )] #[case::front_matter_json_ld_article( r#"

B

"#, - ConversionOptions { - generate_front_matter: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_generate_front_matter(true), "---\nauthor: Ada Lovelace\npublished: 2023-05-01\n---\n\nB", )] #[case::script_tag_leading_newline_stripping( "", - ConversionOptions { - extract_scripts_as_code_blocks: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_extract_scripts_as_code_blocks(true), "```\n var x = 1;\n```", )] #[case::table_with_align_attribute( @@ -1218,34 +1124,22 @@ fn assert_conversion_with_options(html: &str, expected_markdown: &str, options: #[case::h1_not_closed("

Hello World", ConversionOptions::default(), "# Hello World")] #[case::use_title_as_h1_true( "My Document

Body text

", - ConversionOptions { - use_title_as_h1: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_use_title_as_h1(true), "# My Document\n\nBody text" )] #[case::use_title_as_h1_true_with_no_body( "Only Title", - ConversionOptions { - use_title_as_h1: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_use_title_as_h1(true), "# Only Title" )] #[case::use_title_as_h1_true_with_no_title( "

Body only

", - ConversionOptions { - use_title_as_h1: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_use_title_as_h1(true), "Body only" )] #[case::use_title_as_h1_false( "My Document

Body text

", - ConversionOptions { - use_title_as_h1: false, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_use_title_as_h1(false), "Body text" )] #[case::use_title_as_h1_true_with_main( @@ -1253,10 +1147,7 @@ fn assert_conversion_with_options(html: &str, expected_markdown: &str, options: "Page Title", "

Main content

" ), - ConversionOptions { - use_title_as_h1: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_use_title_as_h1(true), "# Page Title\n\nMain content" )] #[case::use_title_as_h1_true_with_article( @@ -1264,10 +1155,7 @@ fn assert_conversion_with_options(html: &str, expected_markdown: &str, options: "Article Title", "

Article content

" ), - ConversionOptions { - use_title_as_h1: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_use_title_as_h1(true), "# Article Title\n\nArticle content" )] #[case::use_title_as_h1_true_with_role_main( @@ -1275,10 +1163,7 @@ fn assert_conversion_with_options(html: &str, expected_markdown: &str, options: "Role Main Title", "

Role main content

" ), - ConversionOptions { - use_title_as_h1: true, - ..ConversionOptions::default() - }, + ConversionOptions::default().with_use_title_as_h1(true), "# Role Main Title\n\nRole main content" )] #[case::pre_with_code_and_text( @@ -1902,10 +1787,7 @@ fn test_html_comment_stripped() { fn test_base_url_resolves_relative_links_and_images() { let html = r#"

a and b and x and already-absolute

"#; - let options = ConversionOptions { - base_url: Some("https://example.com/blog/post/".to_string()), - ..Default::default() - }; + let options = ConversionOptions::default().with_base_url(Some("https://example.com/blog/post/".to_string())); let md = convert_html_to_markdown(html, options).unwrap(); assert!(md.contains("[a](https://example.com/absolute-path)")); assert!(md.contains("[b](https://example.com/blog/sibling)")); @@ -1925,10 +1807,7 @@ fn test_base_href_tag_resolves_relative_links_when_no_explicit_base_url() { fn test_explicit_base_url_takes_priority_over_base_href_tag() { let html = r#"

link

"#; - let options = ConversionOptions { - base_url: Some("https://example.com/blog/".to_string()), - ..Default::default() - }; + let options = ConversionOptions::default().with_base_url(Some("https://example.com/blog/".to_string())); let md = convert_html_to_markdown(html, options).unwrap(); assert!(md.contains("[link](https://example.com/blog/rel)")); } diff --git a/crates/mq-run/src/cli.rs b/crates/mq-run/src/cli.rs index 605614fd9..ede83cd93 100644 --- a/crates/mq-run/src/cli.rs +++ b/crates/mq-run/src/cli.rs @@ -3047,22 +3047,23 @@ impl Cli { fn build_markdown(&self, runtime_values: &[mq_lang::RuntimeValue]) -> mq_markdown::Markdown { let mut markdown = mq_markdown::Markdown::new(runtime_values.iter().flat_map(Self::runtime_value_to_nodes).collect()); - markdown.set_options(mq_markdown::RenderOptions { - list_style: match self.output.list_style.clone() { - ListStyle::Dash => mq_markdown::ListStyle::Dash, - ListStyle::Plus => mq_markdown::ListStyle::Plus, - ListStyle::Star => mq_markdown::ListStyle::Star, - }, - link_title_style: match self.output.link_title_style.clone() { - LinkTitleStyle::Double => mq_markdown::TitleSurroundStyle::Double, - LinkTitleStyle::Single => mq_markdown::TitleSurroundStyle::Single, - LinkTitleStyle::Paren => mq_markdown::TitleSurroundStyle::Paren, - }, - link_url_style: match self.output.link_url_style.clone() { - LinkUrlStyle::None => mq_markdown::UrlSurroundStyle::None, - LinkUrlStyle::Angle => mq_markdown::UrlSurroundStyle::Angle, - }, - }); + markdown.set_options( + mq_markdown::RenderOptions::default() + .with_list_style(match self.output.list_style.clone() { + ListStyle::Dash => mq_markdown::ListStyle::Dash, + ListStyle::Plus => mq_markdown::ListStyle::Plus, + ListStyle::Star => mq_markdown::ListStyle::Star, + }) + .with_link_title_style(match self.output.link_title_style.clone() { + LinkTitleStyle::Double => mq_markdown::TitleSurroundStyle::Double, + LinkTitleStyle::Single => mq_markdown::TitleSurroundStyle::Single, + LinkTitleStyle::Paren => mq_markdown::TitleSurroundStyle::Paren, + }) + .with_link_url_style(match self.output.link_url_style.clone() { + LinkUrlStyle::None => mq_markdown::UrlSurroundStyle::None, + LinkUrlStyle::Angle => mq_markdown::UrlSurroundStyle::Angle, + }), + ); markdown } diff --git a/crates/mq-wasm/src/script.rs b/crates/mq-wasm/src/script.rs index b91e8532c..a61034f2e 100644 --- a/crates/mq-wasm/src/script.rs +++ b/crates/mq-wasm/src/script.rs @@ -228,12 +228,11 @@ pub struct ConversionOptions { impl From for mq_markdown::ConversionOptions { fn from(options: ConversionOptions) -> Self { - Self { - extract_scripts_as_code_blocks: options.extract_scripts_as_code_blocks, - generate_front_matter: options.generate_front_matter, - use_title_as_h1: options.use_title_as_h1, - base_url: Some(options.base_url).filter(|s| !s.is_empty()), - } + mq_markdown::ConversionOptions::default() + .with_extract_scripts_as_code_blocks(options.extract_scripts_as_code_blocks) + .with_generate_front_matter(options.generate_front_matter) + .with_use_title_as_h1(options.use_title_as_h1) + .with_base_url(Some(options.base_url).filter(|s| !s.is_empty())) } } @@ -687,31 +686,38 @@ pub async fn run(code: &str, content: &str, options: JsValue) -> Result mq_markdown::ListStyle::Dash, - ListStyle::Plus => mq_markdown::ListStyle::Plus, - ListStyle::Star => mq_markdown::ListStyle::Star, - }) - .unwrap_or_default(), - link_title_style: options - .link_title_style - .map(|style| match style { - TitleSurroundStyle::Double => mq_markdown::TitleSurroundStyle::Double, - TitleSurroundStyle::Single => mq_markdown::TitleSurroundStyle::Single, - TitleSurroundStyle::Paren => mq_markdown::TitleSurroundStyle::Paren, - }) - .unwrap_or_default(), - link_url_style: options - .link_url_style - .map(|style| match style { - UrlSurroundStyle::Angle => mq_markdown::UrlSurroundStyle::Angle, - UrlSurroundStyle::None => mq_markdown::UrlSurroundStyle::None, - }) - .unwrap_or_default(), - }); + markdown.set_options( + mq_markdown::RenderOptions::default() + .with_list_style( + options + .list_style + .map(|style| match style { + ListStyle::Dash => mq_markdown::ListStyle::Dash, + ListStyle::Plus => mq_markdown::ListStyle::Plus, + ListStyle::Star => mq_markdown::ListStyle::Star, + }) + .unwrap_or_default(), + ) + .with_link_title_style( + options + .link_title_style + .map(|style| match style { + TitleSurroundStyle::Double => mq_markdown::TitleSurroundStyle::Double, + TitleSurroundStyle::Single => mq_markdown::TitleSurroundStyle::Single, + TitleSurroundStyle::Paren => mq_markdown::TitleSurroundStyle::Paren, + }) + .unwrap_or_default(), + ) + .with_link_url_style( + options + .link_url_style + .map(|style| match style { + UrlSurroundStyle::Angle => mq_markdown::UrlSurroundStyle::Angle, + UrlSurroundStyle::None => mq_markdown::UrlSurroundStyle::None, + }) + .unwrap_or_default(), + ), + ); markdown.to_string() }) } diff --git a/crates/mq-web-api/src/api.rs b/crates/mq-web-api/src/api.rs index 4708c3bc4..8d6fc9d69 100644 --- a/crates/mq-web-api/src/api.rs +++ b/crates/mq-web-api/src/api.rs @@ -352,13 +352,12 @@ pub fn check(request: CheckApiRequest) -> CheckApiResponse { /// Formats mq query code using the given formatting options. pub fn format_query(request: FormatApiRequest) -> miette::Result { - let config = FormatterConfig { - indent_width: request.indent_width.unwrap_or(2), - sort_imports: request.sort_imports.unwrap_or(false), - sort_functions: request.sort_functions.unwrap_or(false), - sort_fields: request.sort_fields.unwrap_or(false), - max_width: request.max_width, - }; + let config = FormatterConfig::default() + .with_indent_width(request.indent_width.unwrap_or(2)) + .with_sort_imports(request.sort_imports.unwrap_or(false)) + .with_sort_functions(request.sort_functions.unwrap_or(false)) + .with_sort_fields(request.sort_fields.unwrap_or(false)) + .with_max_width(request.max_width); let formatted = Formatter::new(Some(config)) .format(&request.query) .map_err(|e| miette!("Format error: {}", e))?; @@ -487,6 +486,7 @@ fn type_error_kind(e: &mq_check::TypeError) -> String { mq_check::TypeError::NullablePropagation { .. } => "nullable_propagation", mq_check::TypeError::UnreachableCode { .. } => "unreachable_code", mq_check::TypeError::NonExhaustiveMatch { .. } => "non_exhaustive_match", + _ => "unknown", } .to_string() }