{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "BookSource",
  "description": "v2 书源。",
  "type": "object",
  "required": [
    "bookInfo",
    "content",
    "name",
    "schema",
    "toc",
    "url"
  ],
  "properties": {
    "bookInfo": {
      "$ref": "#/definitions/BookRules"
    },
    "content": {
      "$ref": "#/definitions/ContentRules"
    },
    "explore": {
      "anyOf": [
        {
          "$ref": "#/definitions/ExploreOp"
        },
        {
          "type": "null"
        }
      ]
    },
    "group": {
      "type": "string"
    },
    "http": {
      "default": {
        "charset": "auto",
        "cookies": {},
        "fetcher": "auto",
        "headers": {},
        "warmup": []
      },
      "allOf": [
        {
          "$ref": "#/definitions/Http"
        }
      ]
    },
    "name": {
      "type": "string"
    },
    "samples": {
      "type": "array",
      "items": {
        "$ref": "#/definitions/Sample"
      }
    },
    "schema": {
      "description": "固定为 `\"trnovel-booksource/v2\"`。",
      "type": "string"
    },
    "search": {
      "anyOf": [
        {
          "$ref": "#/definitions/SearchOp"
        },
        {
          "type": "null"
        }
      ]
    },
    "toc": {
      "$ref": "#/definitions/TocRules"
    },
    "url": {
      "description": "站点基址,用于相对链接解析与 `{{base}}`。",
      "type": "string"
    }
  },
  "additionalProperties": false,
  "definitions": {
    "BookRules": {
      "description": "一本书的字段抽取规则(均可省略)。",
      "type": "object",
      "properties": {
        "author": {
          "anyOf": [
            {
              "$ref": "#/definitions/Rule"
            },
            {
              "type": "null"
            }
          ]
        },
        "bookUrl": {
          "description": "列表项:指向书详情页的链接(搜索/浏览结果用;bookInfo 阶段忽略)。",
          "anyOf": [
            {
              "$ref": "#/definitions/Rule"
            },
            {
              "type": "null"
            }
          ]
        },
        "cover": {
          "anyOf": [
            {
              "$ref": "#/definitions/Rule"
            },
            {
              "type": "null"
            }
          ]
        },
        "intro": {
          "anyOf": [
            {
              "$ref": "#/definitions/Rule"
            },
            {
              "type": "null"
            }
          ]
        },
        "kind": {
          "anyOf": [
            {
              "$ref": "#/definitions/Rule"
            },
            {
              "type": "null"
            }
          ]
        },
        "lastChapter": {
          "anyOf": [
            {
              "$ref": "#/definitions/Rule"
            },
            {
              "type": "null"
            }
          ]
        },
        "name": {
          "anyOf": [
            {
              "$ref": "#/definitions/Rule"
            },
            {
              "type": "null"
            }
          ]
        },
        "tocUrl": {
          "anyOf": [
            {
              "$ref": "#/definitions/Rule"
            },
            {
              "type": "null"
            }
          ]
        },
        "wordCount": {
          "anyOf": [
            {
              "$ref": "#/definitions/Rule"
            },
            {
              "type": "null"
            }
          ]
        }
      },
      "additionalProperties": false
    },
    "Category": {
      "description": "浏览分类。",
      "type": "object",
      "required": [
        "title",
        "url"
      ],
      "properties": {
        "title": {
          "type": "string"
        },
        "url": {
          "$ref": "#/definitions/UrlOrRule"
        }
      },
      "additionalProperties": false
    },
    "Charset": {
      "description": "字符集。",
      "type": "string",
      "enum": [
        "auto",
        "utf8",
        "gbk",
        "gb18030",
        "big5"
      ]
    },
    "CleanStep": {
      "description": "单步后处理。",
      "type": "object",
      "properties": {
        "append": {
          "type": [
            "string",
            "null"
          ]
        },
        "prepend": {
          "type": [
            "string",
            "null"
          ]
        },
        "regex": {
          "type": [
            "string",
            "null"
          ]
        },
        "replace": {
          "type": [
            "string",
            "null"
          ]
        },
        "trim": {
          "type": [
            "boolean",
            "null"
          ]
        }
      },
      "additionalProperties": false
    },
    "ContentRules": {
      "description": "正文规则(可选分页)。",
      "type": "object",
      "required": [
        "value"
      ],
      "properties": {
        "maxPages": {
          "default": 100,
          "type": "integer",
          "format": "uint32",
          "minimum": 0.0
        },
        "nextPage": {
          "anyOf": [
            {
              "$ref": "#/definitions/Rule"
            },
            {
              "type": "null"
            }
          ]
        },
        "value": {
          "$ref": "#/definitions/Rule"
        }
      },
      "additionalProperties": false
    },
    "Expect": {
      "description": "样例期望不变量。",
      "type": "object",
      "properties": {
        "minChapters": {
          "type": [
            "integer",
            "null"
          ],
          "format": "uint",
          "minimum": 0.0
        },
        "minContentChars": {
          "type": [
            "integer",
            "null"
          ],
          "format": "uint",
          "minimum": 0.0
        },
        "name": {
          "type": [
            "string",
            "null"
          ]
        },
        "volumes": {
          "type": [
            "integer",
            "null"
          ],
          "format": "uint",
          "minimum": 0.0
        }
      }
    },
    "ExploreOp": {
      "description": "浏览操作。",
      "type": "object",
      "required": [
        "categories",
        "item",
        "list"
      ],
      "properties": {
        "categories": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/Category"
          }
        },
        "item": {
          "$ref": "#/definitions/BookRules"
        },
        "list": {
          "$ref": "#/definitions/Rule"
        }
      },
      "additionalProperties": false
    },
    "Extract": {
      "description": "取值方式(枚举字符串 或 `{ \"attr\": \"...\" }`)。",
      "anyOf": [
        {
          "$ref": "#/definitions/ExtractOp"
        },
        {
          "type": "object",
          "required": [
            "attr"
          ],
          "properties": {
            "attr": {
              "type": "string"
            }
          }
        }
      ]
    },
    "ExtractOp": {
      "description": "文本/HTML 取值算子。",
      "type": "string",
      "enum": [
        "text",
        "ownText",
        "html",
        "innerHtml",
        "outerHtml"
      ]
    },
    "FetchMode": {
      "description": "取页模式:是否动用浏览器解反爬挑战。 真正是否开浏览器还需 app/用户级授权(两级取交集,见 OpenSpec change `browser-fetcher` D12)。",
      "oneOf": [
        {
          "description": "默认:平时 reqwest,撞挑战才升级浏览器。",
          "type": "string",
          "enum": [
            "auto"
          ]
        },
        {
          "description": "永不开浏览器,撞挑战即降级。",
          "type": "string",
          "enum": [
            "reqwest"
          ]
        },
        {
          "description": "整站强制走浏览器(首请求即被挑战 / 整页 JS 渲染)。",
          "type": "string",
          "enum": [
            "browser"
          ]
        }
      ]
    },
    "Http": {
      "description": "HTTP 配置块。",
      "type": "object",
      "properties": {
        "charset": {
          "default": "auto",
          "allOf": [
            {
              "$ref": "#/definitions/Charset"
            }
          ]
        },
        "cookies": {
          "description": "静态 cookie;也是运行时注入 clearance cookie 的落点。",
          "default": {},
          "type": "object",
          "additionalProperties": {
            "type": "string"
          }
        },
        "fetcher": {
          "description": "取页模式(auto|reqwest|browser);默认 auto。",
          "default": "auto",
          "allOf": [
            {
              "$ref": "#/definitions/FetchMode"
            }
          ]
        },
        "headers": {
          "default": {},
          "type": "object",
          "additionalProperties": {
            "type": "string"
          }
        },
        "rateLimit": {
          "anyOf": [
            {
              "$ref": "#/definitions/RateLimit"
            },
            {
              "type": "null"
            }
          ]
        },
        "retry": {
          "anyOf": [
            {
              "$ref": "#/definitions/Retry"
            },
            {
              "type": "null"
            }
          ]
        },
        "timeout": {
          "type": [
            "integer",
            "null"
          ],
          "format": "uint64",
          "minimum": 0.0
        },
        "warmup": {
          "description": "先 GET 这些页以预热会话 cookie。",
          "default": [],
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      },
      "additionalProperties": false
    },
    "LeafRule": {
      "description": "叶子规则:在当前上下文做一次抽取。",
      "type": "object",
      "properties": {
        "clean": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/CleanStep"
          }
        },
        "extract": {
          "default": "text",
          "allOf": [
            {
              "$ref": "#/definitions/Extract"
            }
          ]
        },
        "index": {
          "type": [
            "integer",
            "null"
          ],
          "format": "int64"
        },
        "select": {
          "type": [
            "string",
            "null"
          ]
        },
        "via": {
          "default": "css",
          "allOf": [
            {
              "$ref": "#/definitions/Via"
            }
          ]
        }
      }
    },
    "Method": {
      "description": "HTTP 方法。",
      "type": "string",
      "enum": [
        "GET",
        "POST"
      ]
    },
    "RateLimit": {
      "description": "速率限制。",
      "type": "object",
      "required": [
        "maxCount",
        "perMs"
      ],
      "properties": {
        "maxCount": {
          "type": "integer",
          "format": "uint64",
          "minimum": 0.0
        },
        "perMs": {
          "type": "integer",
          "format": "uint64",
          "minimum": 0.0
        }
      },
      "additionalProperties": false
    },
    "Request": {
      "description": "单个请求。",
      "type": "object",
      "required": [
        "url"
      ],
      "properties": {
        "body": {
          "anyOf": [
            {
              "$ref": "#/definitions/UrlOrRule"
            },
            {
              "type": "null"
            }
          ]
        },
        "headers": {
          "default": {},
          "type": "object",
          "additionalProperties": {
            "type": "string"
          }
        },
        "method": {
          "default": "GET",
          "allOf": [
            {
              "$ref": "#/definitions/Method"
            }
          ]
        },
        "url": {
          "$ref": "#/definitions/UrlOrRule"
        },
        "vars": {
          "description": "命名捕获,供 template 使用。",
          "default": {},
          "type": "object",
          "additionalProperties": {
            "$ref": "#/definitions/Rule"
          }
        }
      },
      "additionalProperties": false
    },
    "Retry": {
      "description": "重试策略。",
      "type": "object",
      "properties": {
        "backoffMs": {
          "default": 0,
          "type": "integer",
          "format": "uint64",
          "minimum": 0.0
        },
        "max": {
          "default": 0,
          "type": "integer",
          "format": "uint32",
          "minimum": 0.0
        }
      },
      "additionalProperties": false
    },
    "Rule": {
      "description": "一条规则:叶子,或组合子。组合子按其唯一键判别(见 design D1)。\n\n反序列化时按变体顺序尝试:组合子(各有唯一必填键)在前,叶子兜底。",
      "anyOf": [
        {
          "description": "取首个非空子规则结果(回退/自愈)。",
          "type": "object",
          "required": [
            "firstOf"
          ],
          "properties": {
            "firstOf": {
              "type": "array",
              "items": {
                "$ref": "#/definitions/Rule"
              }
            }
          }
        },
        {
          "description": "拼接非空子规则结果。",
          "type": "object",
          "required": [
            "concat"
          ],
          "properties": {
            "concat": {
              "type": "array",
              "items": {
                "$ref": "#/definitions/Rule"
              }
            },
            "join": {
              "default": "",
              "type": "string"
            }
          }
        },
        {
          "description": "字面量。",
          "type": "object",
          "required": [
            "literal"
          ],
          "properties": {
            "literal": {
              "type": "string"
            }
          }
        },
        {
          "description": "模板插值(`{{key}}`/`{{page}}`/命名变量)。",
          "type": "object",
          "required": [
            "template"
          ],
          "properties": {
            "template": {
              "type": "string"
            }
          }
        },
        {
          "description": "叶子(兜底)。",
          "allOf": [
            {
              "$ref": "#/definitions/LeafRule"
            }
          ]
        }
      ]
    },
    "Sample": {
      "description": "黄金样例。",
      "type": "object",
      "required": [
        "bookUrl"
      ],
      "properties": {
        "bookUrl": {
          "type": "string"
        },
        "expect": {
          "default": {},
          "allOf": [
            {
              "$ref": "#/definitions/Expect"
            }
          ]
        }
      },
      "additionalProperties": false
    },
    "SearchOp": {
      "description": "搜索操作。",
      "type": "object",
      "required": [
        "item",
        "list",
        "request"
      ],
      "properties": {
        "item": {
          "$ref": "#/definitions/BookRules"
        },
        "list": {
          "$ref": "#/definitions/Rule"
        },
        "request": {
          "$ref": "#/definitions/Request"
        }
      },
      "additionalProperties": false
    },
    "TocRules": {
      "description": "目录规则(章节 + 分卷 + 可选分页)。",
      "type": "object",
      "required": [
        "list",
        "name",
        "url"
      ],
      "properties": {
        "isVolume": {
          "anyOf": [
            {
              "$ref": "#/definitions/Rule"
            },
            {
              "type": "null"
            }
          ]
        },
        "list": {
          "$ref": "#/definitions/Rule"
        },
        "maxPages": {
          "default": 100,
          "type": "integer",
          "format": "uint32",
          "minimum": 0.0
        },
        "name": {
          "$ref": "#/definitions/Rule"
        },
        "nextPage": {
          "anyOf": [
            {
              "$ref": "#/definitions/Rule"
            },
            {
              "type": "null"
            }
          ]
        },
        "url": {
          "$ref": "#/definitions/Rule"
        }
      },
      "additionalProperties": false
    },
    "UrlOrRule": {
      "description": "URL 字段:可为字符串模板,或一条规则。",
      "anyOf": [
        {
          "type": "string"
        },
        {
          "$ref": "#/definitions/Rule"
        }
      ]
    },
    "Via": {
      "description": "抽取后端(决定 `select` 的语义)。",
      "oneOf": [
        {
          "type": "string",
          "enum": [
            "css",
            "xpath",
            "json",
            "regex"
          ]
        },
        {
          "description": "直接使用当前上下文值(只跑 clean)。",
          "type": "string",
          "enum": [
            "raw"
          ]
        }
      ]
    }
  }
}
