Skip to main content

parse_book_source/source/
rule.rs

1//! 规则 AST:抽取语义(`Via`/`Extract`)、叶子规则与组合子(`Rule`)。
2//!
3//! `Rule` 既是配置、也是供求值器([`crate::eval`])遍历的语法树(见 design D1/D6)。
4
5use super::clean::CleanStep;
6use serde::{Deserialize, Serialize};
7
8/// 抽取后端(决定 `select` 的语义)。
9#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Hash)]
10#[serde(rename_all = "lowercase")]
11#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
12pub enum Via {
13    #[default]
14    Css,
15    Xpath,
16    Json,
17    Regex,
18    /// 直接使用当前上下文值(只跑 clean)。
19    Raw,
20}
21
22/// 取值方式(枚举字符串 或 `{ "attr": "..." }`)。
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
24#[serde(untagged)]
25#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
26pub enum Extract {
27    Op(ExtractOp),
28    Attr { attr: String },
29}
30
31impl Default for Extract {
32    fn default() -> Self {
33        Extract::Op(ExtractOp::Text)
34    }
35}
36
37/// 文本/HTML 取值算子。
38#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Hash)]
39#[serde(rename_all = "camelCase")]
40#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
41pub enum ExtractOp {
42    #[default]
43    Text,
44    OwnText,
45    Html,
46    InnerHtml,
47    OuterHtml,
48}
49
50/// 叶子规则:在当前上下文做一次抽取。
51#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, Hash)]
52#[serde(rename_all = "camelCase")]
53#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
54pub struct LeafRule {
55    #[serde(default)]
56    pub via: Via,
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub select: Option<String>,
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub index: Option<i64>,
61    #[serde(default)]
62    pub extract: Extract,
63    #[serde(default, skip_serializing_if = "Vec::is_empty")]
64    pub clean: Vec<CleanStep>,
65}
66
67/// 一条规则:叶子,或组合子。组合子按其唯一键判别(见 design D1)。
68///
69/// 反序列化时按变体顺序尝试:组合子(各有唯一必填键)在前,叶子兜底。
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
71#[serde(untagged)]
72#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
73pub enum Rule {
74    /// 取首个非空子规则结果(回退/自愈)。
75    FirstOf {
76        #[serde(rename = "firstOf")]
77        first_of: Vec<Rule>,
78    },
79    /// 拼接非空子规则结果。
80    Concat {
81        concat: Vec<Rule>,
82        #[serde(default)]
83        join: String,
84    },
85    /// 字面量。
86    Literal { literal: String },
87    /// 模板插值(`{{key}}`/`{{page}}`/命名变量)。
88    Template { template: String },
89    /// JS 逻辑编排逃生舱(值规则):以当前上下文为 `result`、注入 `baseUrl`/变量 + `crypto`
90    /// 助手求值,返回字符串。求值需启用 `js` feature(否则返回 `Unsupported("js")`)。
91    /// 必须置于 `Leaf` 之前——`js` 是其唯一判别键,否则会被全可选的 `Leaf` 吞掉。
92    Js { js: String },
93    /// 叶子(兜底)。
94    Leaf(LeafRule),
95}
96
97impl Rule {
98    /// 决定本规则数据源的「主 `via`」(`render-dual-source` 的双源路由用):叶子取其 `via`,
99    /// 组合子(`firstOf`/`concat`)取首个子规则的主 via,纯值规则(literal/template/js)无源 → `None`。
100    /// 引擎据此把 `via:css`/`xpath` 的规则(如 `totalPages`)路由到渲染 DOM、其余路由到 body。
101    pub fn primary_via(&self) -> Option<Via> {
102        match self {
103            Rule::Leaf(l) => Some(l.via),
104            Rule::FirstOf { first_of } => first_of.first().and_then(Rule::primary_via),
105            Rule::Concat { concat, .. } => concat.first().and_then(Rule::primary_via),
106            Rule::Literal { .. } | Rule::Template { .. } | Rule::Js { .. } => None,
107        }
108    }
109}
110
111/// URL 字段:可为字符串模板,或一条规则。
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
113#[serde(untagged)]
114#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
115pub enum UrlOrRule {
116    Str(String),
117    Rule(Box<Rule>),
118}