Preprocessor

마크다운 본문의 wikilink를 일반 마크다운 링크로 변환하고, 임베딩과 private 태그를 처리하는 전처리 모듈.

전처리는 4단계 파이프라인으로 동작한다: 임베딩 → private 태그 제거 → wikilink 변환 → 스마트 따옴표. 이 순서는 핵심 불변량이다 — 임베딩된 콘텐츠에 {prv} 태그가 있으면 임베딩이 먼저 처리된 후 private 태그가 제거되어야 한다. 또한 코드 블록 내부의 wikilink는 변환하지 않아야 한다.

module preprocess

-- 전처리 파이프라인의 단계
abstract sig Stage {
  order: one Int
}

one sig Embed extends Stage {} { order = 1 }
one sig PrivateTag extends Stage {} { order = 2 }
one sig WikilinkReplace extends Stage {} { order = 3 }
one sig SmartQuote extends Stage {} { order = 4 }

-- 마크다운 토큰: 코드 블록 내부이거나 아니거나
sig Token {
  inCodeBlock: one Bool,
  processedBy: set Stage
}

-- 코드 블록 내부의 토큰은 wikilink 변환 대상이 아님
fact codeBlockProtection {
  all t: Token | t.inCodeBlock = True implies
    WikilinkReplace not in t.processedBy
}

-- 임베딩은 항상 private 태그보다 먼저 처리됨
-- (임베딩된 콘텐츠의 prv 태그가 올바르게 제거되려면)
fact embedBeforePrivate {
  Embed.order < PrivateTag.order
}

-- private 태그는 wikilink보다 먼저 처리됨
-- (prv 태그 안의 wikilink가 불필요하게 변환되지 않으려면)
fact privateBeforeWikilink {
  PrivateTag.order < WikilinkReplace.order
}

-- 코드 블록 보호가 위반되면: wikilink가 코드 안에서도 변환됨
assert codeBlockSafe {
  no t: Token | t.inCodeBlock = True and WikilinkReplace in t.processedBy
}

-- 파이프라인 순서가 전순서(total order)임
assert pipelineIsTotalOrder {
  all disj s1, s2: Stage | s1.order != s2.order
}

check codeBlockSafe for 5
check pipelineIsTotalOrder for 5 but 6 Int

Bool 시그니처는 Alloy 표준 관용구다:

abstract sig Bool {}
one sig True, False extends Bool {}

Private 태그 제거

{prv}...{/prv} 태그는 내용 전체가 제거된다. {prv alt="대체텍스트"}...{/prv}는 대체 텍스트로 치환된다.

mdbaseUrlPathpagenameSetallMetaMapexpected
Before {prv}secret{/prv} After
p
Set()
Map()
Before After
Before {prv alt="공개"}secret{/prv} After
p
Set()
Map()
Before 공개 After

스마트 따옴표 정규화

Curly 따옴표(U+2018 등)를 직선 따옴표로 변환한다.

mdbaseUrlPathpagenameSetallMetaMapexpected
‘hello’ “world”
p
Set()
Map()
'hello' "world"