sprawling-documents 0.0.12

The rules of a document a person reads and edits through a sprawling city's page: versions, characters, windows and saves.
Documentation
-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at https://mozilla.org/MPL/2.0/.
-- Copyright (c) 2026 2youg1 and the sprawling contributors

/-!
# markdown:一个文法把一个版本的窗口读成块

规定 `crates/documents/src/markdown.rs`(`documents::preview`)与它的两个部分 `markdown/tree.rs`、`markdown/lowering.rs`。Rust 代码是「怎样守住」的权威;本模型是「必须守住哪些性质」的权威。CommonMark 与 GFM 的文法本身归 comrak(D20、D22),不在这里重写:模型只管本 crate 在 comrak 的树之外加的三条判定和一条答复的形状。

证明的性质:

1. **链接只指向四类去处**(`admitted`、`other_refused`、`listed_admitted`):相对地址、`http`、`https`、`mailto`;其余的协议(`javascript:`、`data:`、`file:`)不成为链接,它的文字留下(D23)。
2. **嵌套有上限**(`wraps_lower_le`):读出的树套不过 `NESTING_MAX` 层,再深的一层整个成为「不支持」并带着原文;**上限之内一层不丢**(`lower_fits`)(D24)。
3. **预览窗口止于块末,并且总有进展**(`settle_le_cut`、`settle_progress`、`settle_at_end`):窗口不过 `cut` 切出的终点;起点之后只要还有字节,终点就在起点之后,所以逐窗读完整个版本不会停在原地;到了版本末尾的窗口不被截短(D25)。
4. **「不支持」与「内容为空」是两种答复**(`empty_is_not_unsupported`):一个空窗口答零个块,一份本文法不读的编码答「不支持」,两者不相等(A4,D25)。
5. **回复的收束点只进不退,不过它读过的行**(`closed_stays`、`closed_le_length`、`streaming_le_settled`):接着写下去的字不会把已经收束的块收回去;还在说的回复读的不比结算之后多(D30、D31)。顶格开启的代码块闭合时收束,缩进开启的不收束(`a_margin_fence_closes`、`an_indented_fence_closes_nothing`)。

咬得动的演示:不判协议就放过 `javascript:`(`withoutCheck_admits_other`);不设上限,十七层的引用读出十七层(`withoutCap_exceeds`);不要求末尾在起点之后,窗口停在原地(`withoutGuard_stalls`);不认围栏,代码块里的空行被当成收束点(`withoutFence_closes_inside_code`)。
-/

namespace Documents.Markdown

/-! ## 链接的去处(D23)

Rust 先按 WHATWG URL 的读法去掉制表符与换行、去掉开头的控制字符与空格,再取第一个 `:` 之前、合乎 RFC 3986 `scheme` 文法的那一段;没有这一段的是相对地址。本模型从分好的类开始:怎样从字符串分出类是 Rust 的 `markdown::target::scheme_of`,由 `markdown::target::tests` 的表判。 -/

/-- 一个链接目标按协议分成的类。 -/
inductive Scheme where
  | relative
  | http
  | https
  | mailto
  | other
  deriving DecidableEq, Repr

/-- 链接成为链接的条件;不成立时它的文字作为文字留下。 -/
def admitted : Scheme → Bool
  | .relative | .http | .https | .mailto => true
  | .other => false

theorem listed_admitted (s : Scheme) (listed : s ≠ .other) : admitted s = true := by
  cases s <;> simp_all [admitted]

/-- 不判协议的读法:每一个目标都成为链接。 -/
def admittedWithoutCheck : Scheme → Bool := fun _ => true

theorem withoutCheck_admits_other : admittedWithoutCheck .other ≠ admitted .other := by
  decide

/-! ## 嵌套的上限(D24)

一条嵌套链是一串容器(引用、列表项、强调、链接)包着最里面的内容;一棵树的深度是它最深的那条链。`lower room n` 是 Rust 的 `lowering` 在还剩 `room` 层时读 `n`:没有余地时整个成为 `cut`(Rust 的 `Unsupported { construct: Nesting, .. }`,带着原文),否则照原样读出一层、余地减一。 -/

def NESTING_MAX : Nat := 16

inductive Nest where
  | leaf
  | wrap (inner : Nest)

inductive Laid where
  | leaf
  | wrap (inner : Laid)
  | cut

def lower : Nat → Nest → Laid
  | 0, _ => .cut
  | _ + 1, .leaf => .leaf
  | k + 1, .wrap inner => .wrap (lower k inner)

/-- 读出的树套了几层。 -/
def wraps : Laid → Nat
  | .leaf => 0
  | .cut => 0
  | .wrap inner => wraps inner + 1

/-- 源文里的链套了几层。 -/
def depth : Nest → Nat
  | .leaf => 0
  | .wrap inner => depth inner + 1

/-- 照原样读出,不设上限。 -/
def embed : Nest → Laid
  | .leaf => .leaf
  | .wrap inner => .wrap (embed inner)

theorem wraps_lower_le (k : Nat) (n : Nest) : wraps (lower k n) ≤ k := by
  induction k generalizing n with
  | zero => simp [lower, wraps]
  | succ k ih =>
    cases n with
    | leaf => simp [lower, wraps]
    | wrap inner =>
      simp only [lower, wraps]
      have := ih inner
      omega

theorem lower_fits (n : Nest) (k : Nat) (fits : depth n < k) : lower k n = embed n := by
  induction n generalizing k with
  | leaf =>
    cases k with
    | zero => simp [depth] at fits
    | succ k => simp [lower, embed]
  | wrap inner ih =>
    cases k with
    | zero => simp [depth] at fits
    | succ k =>
      simp only [lower, embed]
      rw [ih k (by simp [depth] at fits; omega)]

def deep : Nat → Nest
  | 0 => .leaf
  | k + 1 => .wrap (deep k)

theorem wraps_embed_deep (k : Nat) : wraps (embed (deep k)) = k := by
  induction k with
  | zero => rfl
  | succ k ih => simp [deep, embed, wraps, ih]

theorem withoutCap_exceeds : NESTING_MAX < wraps (embed (deep (NESTING_MAX + 1))) := by
  rw [wraps_embed_deep]
  omega

/-! ## 预览窗口止于块末(D25)

`cutEnd` 是 `documents::cut` 切出的终点(不劈开字符,不过 `WINDOW_BYTES_MAX`,`spec/Window.lean`),`size` 是版本的长度。窗口没到版本末尾时,它最后一块可能还在窗口之外接着写,所以窗口止于倒数第二块的末尾 `penultimate`(Rust 里是 `layout::blocks` 读窗口字节得到的块,加上窗口的起点);没有倒数第二块、或它不在起点之后,就留在 `cutEnd`。 -/

def settle (start cutEnd size : Nat) (penultimate : Option Nat) : Nat :=
  if size ≤ cutEnd then cutEnd
  else match penultimate with
    | some e => if start < e then e else cutEnd
    | none => cutEnd

theorem settle_le_cut (start cutEnd size : Nat) (p : Option Nat)
    (within : ∀ e, p = some e → e ≤ cutEnd) : settle start cutEnd size p ≤ cutEnd := by
  cases p with
  | none =>
    show (if size ≤ cutEnd then cutEnd else cutEnd) ≤ cutEnd
    split <;> exact Nat.le_refl _
  | some e =>
    have inside := within e rfl
    show (if size ≤ cutEnd then cutEnd else if start < e then e else cutEnd) ≤ cutEnd
    split
    · exact Nat.le_refl _
    · split
      · exact inside
      · exact Nat.le_refl _

theorem settle_progress (start cutEnd size : Nat) (p : Option Nat) (more : start < cutEnd) :
    start < settle start cutEnd size p := by
  cases p with
  | none =>
    show start < (if size ≤ cutEnd then cutEnd else cutEnd)
    split <;> exact more
  | some e =>
    show start < (if size ≤ cutEnd then cutEnd else if start < e then e else cutEnd)
    split
    · exact more
    · split
      · assumption
      · exact more

theorem settle_at_end (start cutEnd size : Nat) (p : Option Nat) (atEnd : size ≤ cutEnd) :
    settle start cutEnd size p = cutEnd := by
  unfold settle
  simp [atEnd]

/-- 不要求倒数第二块的末尾在起点之后。 -/
def settleWithoutGuard (cutEnd size : Nat) (p : Option Nat) : Nat :=
  if size ≤ cutEnd then cutEnd else p.getD cutEnd

theorem withoutGuard_stalls : ¬ (3 < settleWithoutGuard 10 20 (some 3)) := by
  decide

/-! ## 「不支持」与「内容为空」(A4,D25)

答复的形状:读出的块(可以是零个),或者「这份版本的编码本文法不读」。Rust 里是 `documents::Preview::{Laid, Unsupported}`;块一级的「不支持」是 `Block::Unsupported`,与一个内容为空的代码块 `Block::Code { text: "" }` 同样是两个构造子。 -/

inductive Preview where
  | laid (blocks : Nat)
  | unsupported

/-! ## 回复里已经不会再变的块(D30、D31)

Rust 的 `layout::closed` 逐行读一段还在写的 Markdown,只读以换行结束的完整行,答最后一个收束点之后的字节位置。模型以行计位置:字节怎样切成行、一行属于哪一类是 Rust 的 `layout` 的事(`layout::tests::closed_*` 判),这里从分好类的行开始。围栏的界定符 `D` 与「这一行闭合那一个」的关系 `closes` 是参数:Rust 里是 `layout::Delimiter` 与它的 `closes`(同一字符、不短于开启行)。 -/

/-- 一个围栏行从第一列开始,还是缩进了一到三列。 -/
inductive Margin where
  | atMargin
  | indented
  deriving DecidableEq, Repr

/-- 一行完整的字按收束规则分的类:只有空白、顶格的 ATX 标题、围栏、其余。Rust 先判围栏,再判空白与标题,所以四类不相交。 -/
inductive Line (D : Type) where
  | blank
  | heading
  | fence (delimiter : D) (margin : Margin)
  | other

/-- 读到某一行时的状态:开着的代码块(它的界定符与开启处)、读过几行、收束点在第几行之后。 -/
structure Scan (D : Type) where
  code : Option (D × Margin)
  seen : Nat
  closed : Nat

def Scan.init {D : Type} : Scan D := ⟨none, 0, 0⟩

/-- 这一行是不是收束点:代码块之外的空行与顶格标题是;闭合一个顶格开启的代码块的那一行是;代码块里的其余一切都只是代码。 -/
def closesHere {D : Type} (closes : D → D → Bool) : Option (D × Margin) → Line D → Bool
  | none, .blank => true
  | none, .heading => true
  | some (o, .atMargin), .fence d _ => closes d o
  | _, _ => false

/-- 读完这一行之后开着的代码块:代码块之外的围栏开启一个,闭合它的围栏关上它。 -/
def nextCode {D : Type} (closes : D → D → Bool) : Option (D × Margin) → Line D → Option (D × Margin)
  | none, .fence d m => some (d, m)
  | some (o, om), .fence d _ => if closes d o then none else some (o, om)
  | code, _ => code

def step {D : Type} (closes : D → D → Bool) (s : Scan D) (l : Line D) : Scan D :=
  { code := nextCode closes s.code l
    seen := s.seen + 1
    closed := if closesHere closes s.code l then s.seen + 1 else s.closed }

/-- 收束点:读完这些完整行之后,已经不会再变的块止于第几行之后。 -/
def closedUpTo {D : Type} (closes : D → D → Bool) (lines : List (Line D)) : Nat :=
  (lines.foldl (step closes) Scan.init).closed

theorem step_moves_on {D : Type} (closes : D → D → Bool) (s : Scan D) (l : Line D)
    (inv : s.closed ≤ s.seen) :
    s.closed ≤ (step closes s l).closed ∧ (step closes s l).closed ≤ (step closes s l).seen
      ∧ (step closes s l).seen = s.seen + 1 := by
  show s.closed ≤ (if closesHere closes s.code l then s.seen + 1 else s.closed)
    ∧ (if closesHere closes s.code l then s.seen + 1 else s.closed) ≤ s.seen + 1
    ∧ s.seen + 1 = s.seen + 1
  by_cases here : closesHere closes s.code l = true <;> simp [here] <;> omega

theorem fold_moves_on {D : Type} (closes : D → D → Bool) (lines : List (Line D)) :
    ∀ s : Scan D, s.closed ≤ s.seen →
      s.closed ≤ (lines.foldl (step closes) s).closed
        ∧ (lines.foldl (step closes) s).closed ≤ (lines.foldl (step closes) s).seen
        ∧ (lines.foldl (step closes) s).seen = s.seen + lines.length := by
  induction lines with
  | nil => intro s inv; simp only [List.foldl_nil, List.length_nil]; omega
  | cons l rest ih =>
    intro s inv
    obtain ⟨grows, within, counted⟩ := step_moves_on closes s l inv
    obtain ⟨grows', within', counted'⟩ := ih (step closes s l) within
    simp only [List.foldl_cons, List.length_cons]
    exact ⟨Nat.le_trans grows grows', within', by omega⟩

/-- 收束点不过读过的行。 -/
theorem closed_le_length {D : Type} (closes : D → D → Bool) (lines : List (Line D)) :
    closedUpTo closes lines ≤ lines.length := by
  obtain ⟨_, within, counted⟩ := fold_moves_on closes lines Scan.init (Nat.le_refl 0)
  unfold closedUpTo
  rw [counted] at within
  simpa [Scan.init] using within

/-- 收束点只进不退:接着写下去的行不会把已经收束的块收回去。 -/
theorem closed_stays {D : Type} (closes : D → D → Bool) (lines more : List (Line D)) :
    closedUpTo closes lines ≤ closedUpTo closes (lines ++ more) := by
  unfold closedUpTo
  rw [List.foldl_append]
  obtain ⟨_, within, _⟩ := fold_moves_on closes lines Scan.init (Nat.le_refl 0)
  exact (fold_moves_on closes more _ within).1

/-- 一段回复的状态:还在说,或已结算(Rust 的 `ReplyState`)。 -/
inductive ReplyState where
  | streaming
  | settled

/-- `reply` 读到第几行之前:结算了的回复整段读,还在说的读到收束点。 -/
def reach {D : Type} (closes : D → D → Bool) : ReplyState → List (Line D) → Nat
  | .settled, lines => lines.length
  | .streaming, lines => closedUpTo closes lines

theorem streaming_le_settled {D : Type} (closes : D → D → Bool) (lines : List (Line D)) :
    reach closes .streaming lines ≤ reach closes .settled lines :=
  closed_le_length closes lines

/-- 界定符相同即闭合的一个具体例子,演示用。 -/
def sameMark (closing opened : Nat) : Bool := closing == opened

/-- 不认围栏的读法:围栏行当作其余的行。 -/
def withoutFences {D : Type} : Line D → Line D
  | .fence _ _ => .other
  | l => l

/-- 一个还开着的代码块里有一个空行:认围栏时什么也没收束,不认时空行成了收束点,代码块的前半被当成读完了的块。 -/
theorem withoutFence_closes_inside_code :
    closedUpTo sameMark ([.fence 3 .atMargin, .blank, .other].map withoutFences)
      ≠ closedUpTo sameMark [.fence 3 .atMargin, .blank, .other] := by
  decide

end Documents.Markdown