
Csl
- 19 installs
- 45 repo stars
- Updated June 23, 2026
- yuanyuanma03/academic-research-skills
Generates a Zotero-compatible CSL citation style file from a described format such as GB/T 7714, APA, or a custom spec.
About
Builds a Zotero-usable CSL style file from the user's described citation format, using bundled presets, components, and validation scripts. A researcher uses it to create a custom citation style for reference management.
- Generates CSL from GB/T 7714, APA, or custom rules
- Includes validate and preview scripts
Csl by the numbers
- 19 all-time installs (skills.sh)
- Ranked #1,308 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/yuanyuanma03/academic-research-skills --skill cslAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 19 |
|---|---|
| repo stars | ★ 45 |
| Last updated | June 23, 2026 |
| Repository | yuanyuanma03/academic-research-skills ↗ |
What it does
Generates a Zotero-compatible CSL citation style file from a described format such as GB/T 7714, APA, or a custom spec.
Files
CSL 样式生成器
所有模板、参数、规则均位于 . 目录下。
以下路径常量贯穿全流程:
| 常量 | 路径 |
|---|---|
| 预设目录 | presets/ |
| 组件目录 | components/ |
| 校验脚本 | scripts/validate_csl.py |
| 预览脚本 | scripts/preview_csl.py |
| 校验规则 | validate/rules.md |
| 输出目录 | output/ |
Step 0: 收集信息
分析用户输入 $ARGUMENTS,判断信息是否充足。如果不足,必须先向用户确认以下内容再开始生成:
1. 正文引用方式(三选一,决定 CSL class 和 citation 配置):
- 上标角标:正文中
[1-3]以上标形式出现 →class="in-text",vertical-align="sup" - 行内编号:正文中
[1-3]与正文同行同字号 →class="in-text" - 脚注/尾注:正文中插入脚注标记,引用内容出现在页脚或文末 →
class="note"
2. 参考文献列表示例:至少需要用户提供 2-3 条不同类型(期刊、书籍、会议等)的参考文献原文,用于反推格式参数。
3. 正文引用示例(可选):包含引用标记的原文段落,用于确认引用样式。
如果用户已经提供了上述信息(如粘贴了参考文献和正文),直接进入 Step 1 分析。
Step 1: 解析需求
从用户输入提取格式需求,匹配预设关键词:
| 关键词 | 预设文件 |
|---|---|
| GB/T 7714、国标、中文顺序编码 | gbt7714-numeric.md |
| GB/T 7714 著者-出版年、author-date | gbt7714-author-date.md |
| APA | apa7.md |
| Chicago Notes、芝加哥脚注 | chicago-notes.md |
| IEEE | ieee.md |
| MLA | mla9.md |
| 中文社科 note、脚注样式 | chinese-note.md |
- 匹配到 → 进入 Step 2a
- 未匹配 → 进入 Step 2b(从用户描述或参考文献示例反推参数)
Step 2: 读取配方
2a 预设路径: 读取 presets/{preset}.md,获取全部参数。
2b 自定义路径: 按需读取 components/ 下的组件模板:
| 组件 | 文件 | 职责 |
|---|---|---|
| 作者 | name.md | 姓名格式、et-al、排序 |
| 标题 | title.md | 斜体/引号/书名号 |
| 日期 | date.md | 年/月/日格式 |
| 期刊/书籍容器 | container.md | 期刊名、书名格式 |
| 卷期页 | locators.md | 卷/期/页码格式 |
| 出版信息 | publisher.md | 出版地、出版社 |
| DOI/URL | access.md | 电子资源访问信息 |
| 正文引用 | citation.md | citation 布局 |
| 参考文献列表 | bibliography.md | bibliography 布局 |
| 中文术语 | locale-zh.md | 中文本地化术语 |
| 英文术语 | locale-en.md | 英文本地化术语 |
根据用户需求选择性读取相关组件,无需全部加载。
Step 3: 生成 CSL
按以下骨架组装完整 .csl 文件:
style (xmlns, class, version, default-locale)
├── info (title, id, category, updated)
├── locale × N (术语覆盖)
├── macro × N (按组件模板填充)
├── citation (正文引用 / 脚注)
└── bibliography (参考文献列表)- 各宏的 XML 实现从组件模板中获取,按预设参数调整属性值
- 输出到
output/{style-name}.csl
Step 4: 校验
python scripts/validate_csl.py <file>- 脚本输出 JSON 格式结果
- 如
"status": "FAIL",根据 errors 修复后重新校验,直到 PASS - 读取
validate/rules.md做补充审核(常见陷阱检查)
Step 5: 预览
python scripts/preview_csl.py <file>- 展示真实渲染结果给用户
- 等待用户确认,或根据反馈返回 Step 3 修改
Step 6: 修改已有样式(可选)
当用户提供现有 .csl 文件要求修改时: 1. 读取文件,定位需修改的部分 2. 参考对应组件模板进行修改 3. 重新执行 Step 4 + Step 5
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
preview_csl.py — 用 citeproc-py 加载 .csl 文件和测试数据,渲染 citation 和 bibliography 输出。
用法:
python preview_csl.py <file.csl> # 使用默认 test_data.json
python preview_csl.py <file.csl> --data custom.json # 使用自定义数据
"""
import argparse
import json
import os
import sys
# Windows 下强制 UTF-8 输出,避免中文乱码
if sys.platform == "win32":
os.environ.setdefault("PYTHONUTF8", "1")
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
if hasattr(sys.stderr, "reconfigure"):
sys.stderr.reconfigure(encoding="utf-8")
# ---------------------------------------------------------------------------
# 依赖检查
# ---------------------------------------------------------------------------
try:
from citeproc import (
Citation,
CitationItem,
CitationStylesBibliography,
CitationStylesStyle,
formatter,
)
from citeproc.source.json import CiteProcJSON
except ImportError:
print("错误: 缺少 citeproc-py 库。请先安装:")
print(" pip install citeproc-py")
sys.exit(1)
# ---------------------------------------------------------------------------
# 辅助函数
# ---------------------------------------------------------------------------
def load_test_data(data_path: str) -> list[dict]:
"""加载 CSL-JSON 格式的测试数据。"""
with open(data_path, "r", encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, list) or len(data) == 0:
print(f"错误: {data_path} 应为非空 JSON 数组")
sys.exit(1)
return data
def warn_callback(citation_warning):
"""citeproc 回调,用于捕获警告(这里静默忽略)。"""
pass
def render_citation_text(bib: CitationStylesBibliography, item_ids: list[str]) -> str:
"""注册并渲染一个 citation,返回字符串。"""
citation = Citation([CitationItem(item_id) for item_id in item_ids])
bib.register(citation)
return str(bib.cite(citation, warn_callback))
def preview_csl(csl_path: str, data_path: str) -> None:
"""主流程:加载 CSL 和数据,输出 citation + bibliography。"""
# --- 加载数据 ---
test_data = load_test_data(data_path)
item_ids = [entry["id"] for entry in test_data]
# --- 加载 CSL 样式 ---
try:
style = CitationStylesStyle(csl_path, validate=False)
except Exception as exc:
print(f"错误: 无法加载 CSL 文件 '{csl_path}': {exc}")
sys.exit(1)
# --- 构建 bibliography ---
source = CiteProcJSON(test_data)
bib = CitationStylesBibliography(style, source, formatter.plain)
# === Citation (正文引用) ===
print("=== Citation (正文引用) ===")
# Single: 第 1 条
single = render_citation_text(bib, [item_ids[0]])
print(f"Single: {single}")
# Multiple: 第 1 条 + 第 3 条(不连续)
if len(item_ids) >= 3:
multiple = render_citation_text(bib, [item_ids[0], item_ids[2]])
print(f"Multiple: {multiple}")
# Range: 前 3 条(连续)
if len(item_ids) >= 3:
range_cite = render_citation_text(bib, [item_ids[0], item_ids[1], item_ids[2]])
print(f"Range: {range_cite}")
# --- 为剩余未注册的条目也生成 citation,确保 bibliography 包含所有条目 ---
registered = set()
registered.update(item_ids[:3] if len(item_ids) >= 3 else item_ids[:1])
remaining = [iid for iid in item_ids if iid not in registered]
if remaining:
rest_citation = Citation([CitationItem(iid) for iid in remaining])
bib.register(rest_citation)
bib.cite(rest_citation, warn_callback)
# === Bibliography (参考文献列表) ===
print()
print("=== Bibliography (参考文献列表) ===")
bibliography = bib.bibliography()
if bibliography:
for item in bibliography:
text = str(item).strip()
if text:
print(text)
else:
print("(bibliography 为空,请检查 CSL 文件配置)")
# ---------------------------------------------------------------------------
# CLI 入口
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="用 citeproc-py 预览 CSL 样式的 citation 和 bibliography 输出"
)
parser.add_argument("csl_file", help="CSL 样式文件路径")
parser.add_argument(
"--data",
default=None,
help="CSL-JSON 测试数据文件路径 (默认使用同目录下的 test_data.json)",
)
args = parser.parse_args()
# CSL 文件
csl_path = os.path.abspath(args.csl_file)
if not os.path.isfile(csl_path):
print(f"错误: CSL 文件不存在: {csl_path}")
sys.exit(1)
# 数据文件
if args.data:
data_path = os.path.abspath(args.data)
else:
data_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "test_data.json")
if not os.path.isfile(data_path):
print(f"错误: 数据文件不存在: {data_path}")
sys.exit(1)
print(f"CSL: {csl_path}")
print(f"Data: {data_path}")
print()
preview_csl(csl_path, data_path)
if __name__ == "__main__":
main()
namespace a = "http://relaxng.org/ns/compatibility/annotations/1.0"
## Categories for style metadata
div {
category.citation-format =
"author" | "author-date" | "label" | "note" | "numeric"
## Use "generic-base" for styles that are non-discipline specific, such as
## APA, Harvard, etc.
category.field =
"anthropology"
| "astronomy"
| "biology"
| "botany"
| "chemistry"
| "communications"
| "engineering"
| "generic-base"
| "geography"
| "geology"
| "history"
| "humanities"
| "law"
| "linguistics"
| "literature"
| "math"
| "medicine"
| "philosophy"
| "physics"
| "political_science"
| "psychology"
| "science"
| "social_science"
| "sociology"
| "theology"
| "zoology"
}
namespace a = "http://relaxng.org/ns/compatibility/annotations/1.0"
namespace cs = "http://purl.org/net/xbiblio/csl"
## cs:choose - Conditional Statements"
div {
rendering-element.choose =
## Use to conditionally render rendering elements.
element cs:choose { choose.if, choose.else-if*, choose.else? }
choose.if = element cs:if { condition+, match, rendering-element* }
choose.else-if =
element cs:else-if { condition+, match, rendering-element* }
choose.else = element cs:else { rendering-element+ }
condition =
## If used, the element content is only rendered if it disambiguates two
## otherwise identical citations. This attempt at disambiguation is only
## made after all other disambiguation methods have failed.
[ a:defaultValue = "true" ] attribute disambiguate { "true" }
|
## Tests whether the given variables contain numeric text.
attribute is-numeric {
list { variables+ }
}
|
## Tests whether the given date variables contain approximate dates.
attribute is-uncertain-date {
list { variables.dates+ }
}
|
## Tests whether the locator matches the given locator types.
attribute locator {
list { terms.locator+ }
}
|
## Tests whether the cite position matches the given positions.
attribute position {
list {
("first"
| "subsequent"
| "ibid"
| "ibid-with-locator"
| "near-note")+
}
}
|
## Tests whether the item matches the given types.
attribute type {
list { item-types+ }
}
|
## Tests whether the default ("long") forms of the given variables
## contain non-empty values.
attribute variable {
list { variables+ }
}
match =
## Set the testing logic.
[ a:defaultValue = "all" ]
attribute match {
## Element only tests "true" when all conditions test "true" for all
## given test values.
"all"
|
## Element tests "true" when any condition tests "true" for any given
## test value.
"any"
|
## Element only tests "true" when none of the conditions test "true"
## for any given test value.
"none"
}?
}
namespace a = "http://relaxng.org/ns/compatibility/annotations/1.0"
## Terms
div {
terms =
terms.gender-assignable
| terms.gender-variants
| terms.locator
| item-types
|
## Contributor roles
variables.names
| "editortranslator"
|
## Miscellaneous terms
"accessed"
| "ad"
| "advance-online-publication"
| "album"
| "and"
| "and others"
| "anonymous"
| "at"
| "audio-recording"
| "available at"
| "bc"
| "bce"
| "by"
| "ce"
| "circa"
| "cited"
| "et-al"
| "film"
| "forthcoming"
| "from"
| "henceforth"
| "ibid"
| "in"
| "in press"
| "internet"
| "interview"
| "letter"
| "loc-cit"
| "no date"
| "no-place"
| "no-publisher"
| "on"
| "online"
| "op-cit"
| "original-work-published"
| "personal-communication"
| "podcast"
| "podcast-episode"
| "preprint"
| "presented at"
| "radio-broadcast"
| "radio-series"
| "radio-series-episode"
| "reference"
| "retrieved"
| "review-of"
| "scale"
| "special-issue"
| "special-section"
| "television-broadcast"
| "television-series"
| "television-series-episode"
| "video"
| "working-paper"
|
## Punctuation
"open-quote"
| "close-quote"
| "open-inner-quote"
| "close-inner-quote"
| "page-range-delimiter"
| "colon"
| "comma"
| "semicolon"
|
## Seasons
"season-01"
| "season-02"
| "season-03"
| "season-04"
## Terms to which a gender may be assigned
terms.gender-assignable =
## Months
"month-01"
| "month-02"
| "month-03"
| "month-04"
| "month-05"
| "month-06"
| "month-07"
| "month-08"
| "month-09"
| "month-10"
| "month-11"
| "month-12"
| terms.non-locator-number-variables
| terms.locator-number-variables
## Terms for which gender variants may be specified
terms.gender-variants = terms.ordinals | terms.long-ordinals
terms.ordinals =
## Ordinals
xsd:string { pattern = "ordinal(-\d{2})?" }
terms.long-ordinals =
## Long ordinals
"long-ordinal-01"
| "long-ordinal-02"
| "long-ordinal-03"
| "long-ordinal-04"
| "long-ordinal-05"
| "long-ordinal-06"
| "long-ordinal-07"
| "long-ordinal-08"
| "long-ordinal-09"
| "long-ordinal-10"
## Locators
terms.locator =
"act"
| "appendix"
| "article-locator"
| "book"
| "canon"
| "chapter"
| "column"
| "elocation"
| "equation"
| "figure"
| "folio"
| "line"
| "note"
| "opus"
| "paragraph"
| "rule"
| "scene"
| "sub-verbo"
| "table"
| "timestamp"
| "title-locator"
| "verse"
| terms.locator-number-variables
## Locator terms with matching number variables
terms.locator-number-variables =
"issue"
| "page"
| "part"
| "section"
| "supplement"
| "version"
| "volume"
## Non-locator terms accompanying number variables
terms.non-locator-number-variables =
"chapter-number"
| "citation-number"
| "collection-number"
| "edition"
| "first-reference-note-number"
| "number"
| "number-of-pages"
| "number-of-volumes"
| "page-first"
| "printing"
}
namespace a = "http://relaxng.org/ns/compatibility/annotations/1.0"
## Item types
div {
item-types =
"article"
| "article-journal"
| "article-magazine"
| "article-newspaper"
| "bill"
| "book"
| "broadcast"
| "chapter"
| "classic"
| "collection"
| "dataset"
| "document"
| "entry"
| "entry-dictionary"
| "entry-encyclopedia"
| "event"
| "figure"
| "graphic"
| "hearing"
| "interview"
| "legal_case"
| "legislation"
| "manuscript"
| "map"
| "motion_picture"
| "musical_score"
| "pamphlet"
| "paper-conference"
| "patent"
| "performance"
| "periodical"
| "personal_communication"
| "post"
| "post-weblog"
| "regulation"
| "report"
| "review"
| "review-book"
| "software"
| "song"
| "speech"
| "standard"
| "thesis"
| "treaty"
| "webpage"
}
namespace a = "http://relaxng.org/ns/compatibility/annotations/1.0"
## Variables
div {
## All variables
variables = variables.dates | variables.names | variables.standard
## Standard variables
variables.standard =
variables.numbers | variables.strings | variables.titles
## Date variables
variables.dates =
"accessed"
| "available-date"
| "event-date"
| "issued"
| "original-date"
| "submitted"
## Name variables
variables.names =
"author"
| "chair"
| "collection-editor"
| "compiler"
| "composer"
| "container-author"
| "contributor"
| "curator"
| "director"
| "editor"
| "editor-translator"
| "editorial-director"
| "executive-producer"
| "guest"
| "host"
| "illustrator"
| "interviewer"
| "narrator"
| "organizer"
| "original-author"
| "performer"
| "producer"
| "recipient"
| "reviewed-author"
| "script-writer"
| "series-creator"
| "translator"
## Number variables
variables.numbers =
"chapter-number"
| "citation-number"
| "collection-number"
| "edition"
| "first-reference-note-number"
| "issue"
| "locator"
| "number"
| "number-of-pages"
| "number-of-volumes"
| "page"
| "page-first"
| "part-number"
| "printing-number"
| "section"
| "supplement-number"
| "version"
| "volume"
## Title variables
variables.titles =
"collection-title"
| "container-title"
| "original-title"
| "part-title"
| "reviewed-title"
| "title"
| "volume-title"
| # Short title forms. Will be removed in CSL 1.1
"title-short"
| "container-title-short"
## String variables
variables.strings =
"abstract"
| "annote"
| "archive"
| "archive_collection"
| "archive_location"
| "archive-place"
| "authority"
| "call-number"
| "citation-key"
| "citation-label"
| "dimensions"
| "division"
| "DOI"
| # Alias for 'event-title'. Deprecated. Will be removed in CSL 1.1.
"event"
| "event-title"
| "event-place"
| "genre"
| "ISBN"
| "ISSN"
| "jurisdiction"
| "keyword"
| "language"
| "license"
| "medium"
| "note"
| "original-publisher"
| "original-publisher-place"
| "PMCID"
| "PMID"
| "publisher"
| "publisher-place"
| "references"
| "reviewed-genre"
| "scale"
| "source"
| "status"
| "URL"
| "year-suffix"
}
namespace a = "http://relaxng.org/ns/compatibility/annotations/1.0"
namespace bibo = "http://purl.org/ontology/bibo/"
namespace cs = "http://purl.org/net/xbiblio/csl"
namespace dc = "http://purl.org/dc/elements/1.1/"
namespace sch = "http://purl.oclc.org/dsdl/schematron"
namespace xhtml = "http://www.w3.org/1999/xhtml"
# CSL schema metadata
dc:title [ "Citation Style Language" ]
dc:creator [ "Bruce D'Arcus" ]
dc:creator [ "Simon Kornblith" ]
bibo:editor [ "Frank Bennett" ]
bibo:editor [ "Rintze Zelle" ]
dc:rights [
"Copyright 2007-2020 Citation Style Language and contributors"
]
dc:license [ "MIT license" ]
dc:description [
"RELAX NG compact schema for the Citation Style Language (CSL)."
]
## Subparts of the CSL schema
include "csl-choose.rnc"
include "csl-terms.rnc"
include "csl-types.rnc"
include "csl-variables.rnc"
include "csl-categories.rnc"
# ==============================================================================
## cs:style and cs:locale - Root Elements
div {
start =
independent-style.style | dependent-style.style | locale-file.locale
independent-style.style =
element cs:style {
## Select whether citations appear in-text or as notes.
attribute class { "in-text" | "note" },
style.default-locale,
style.options,
version,
independent-style.style.info,
(style.locale*
& style.macro*
& style.citation
& style.bibliography?)
}
dependent-style.style =
element cs:style {
style.default-locale, version, dependent-style.style.info
}
style.default-locale =
## Set a default style locale.
attribute default-locale { xsd:language }?
version =
## Indicate CSL version compatibility.
[ a:defaultValue = "1.0" ] attribute version { "1.0" }
}
# ==============================================================================
## cs:info - Style and Locale File Metadata
div {
## Metadata for independent styles.
independent-style.style.info =
element cs:info {
info.author*
& info.category*
& info.contributor*
& info.id
& info.issn*
& info.eissn?
& info.issnl?
& independent-style.info.link*
& info.published?
& info.rights?
& info.summary?
& info.title
& info.title-short?
& info.updated
}
## Metadata for dependent styles.
dependent-style.style.info =
element cs:info {
info.author*
& info.category*
& info.contributor*
& info.id
& info.issn*
& info.eissn?
& info.issnl?
& dependent-style.info.link+
& info.published?
& info.rights?
& info.summary?
& info.title
& info.title-short?
& info.updated
}
## Metadata for locale files.
locale-file.locale.info =
element cs:info { info.translator* & info.rights? & info.updated? }
info.author = element cs:author { personal-details }
info.contributor = element cs:contributor { personal-details }
info.translator = element cs:translator { personal-details }
personal-details =
element cs:name { text }
& element cs:email { text }?
& element cs:uri { xsd:anyURI }?
info.category =
## Specify the citation format of the style (using the "citation-format"
## attribute) or the fields and disciplines for which the style is
## relevant (using the "field" attribute).
element cs:category {
attribute citation-format { category.citation-format }
| attribute field { category.field }
}
info.id =
## Specify the unique and stable identifier for the style. A URI
## is valid, but new styles should use a UUID to ensure stability
## and uniqueness.
element cs:id { xsd:anyURI }
info.issn =
## Specify the journal's ISSN(s) for journal-specific styles. An ISSN
## must consist of four digits, a hyphen, three digits, and a check
## digit (a numeral digit or roman X), e.g. "1234-1231".
element cs:issn { issn }
info.eissn =
## Specify the journal's eISSN for journal-specific styles.
element cs:eissn { issn }
info.issnl =
## Specify the journal's ISSN-L for journal-specific styles.
element cs:issnl { issn }
issn = xsd:string { pattern = "\d{4}\-\d{3}(\d|x|X)" }
independent-style.info.link =
element cs:link {
attribute href { xsd:anyURI },
## Specify how the URL relates to the style.
attribute rel {
## The URI of the CSL style itself.
"self"
|
## URI of the style from which the current style is derived.
"template"
|
## URI of style documentation.
"documentation"
},
info-text
}
dependent-style.info.link =
element cs:link {
attribute href { xsd:anyURI },
## Specify how the URL relates to the style.
attribute rel {
## The URI of the CSL style itself.
"self"
|
## URI of the CSL style whose content should be used for
## processing. Required for dependent styles.
"independent-parent"
|
## URI of style documentation.
"documentation"
},
info-text
}
info.published =
## Specify when the style was initially created or made available.
element cs:published { xsd:dateTime }
info.rights =
element cs:rights {
attribute license { xsd:anyURI }?,
info-text
}
info.summary = element cs:summary { info-text }
info.title = element cs:title { info-text }
info.title-short =
## Specify an abbreviated style title (e.g., "APA")
element cs:title-short { info-text }
info.updated =
## Specify when the style was last updated (e.g.,
## "2007-10-26T21:32:52+02:00")
element cs:updated { xsd:dateTime }
info-text =
attribute xml:lang { xsd:language }?,
text
}
# ==============================================================================
## cs:locale in Independent Styles
div {
style.locale =
## Use to (re)define localized terms, dates and options.
element cs:locale {
## Specify the affected locale(s). If "xml:lang" is not set, the
## "cs:locale" element affects all locales.
attribute xml:lang { xsd:language }?,
(locale.style-options? & locale.date* & locale.terms?)
}
}
# ==============================================================================
## cs:locale Contents - Localization Data
div {
## Localized global options are specified as attributes in the
## cs:style-options element. If future versions of CSL include localized
## options that are citation or bibliography specific, the elements
## cs:citation-options and cs:bibliography-options can be added.
locale.style-options =
element cs:style-options {
## Limit the "ordinal" form to the first day of the month.
[ a:defaultValue = "false" ]
attribute limit-day-ordinals-to-day-1 { xsd:boolean }?,
## Specify whether punctuation (a period or comma) is placed within
## or outside (default) the closing quotation mark.
[ a:defaultValue = "false" ]
attribute punctuation-in-quote { xsd:boolean }?
}
locale-file.locale =
element cs:locale {
## Specify the locale of the locale file.
attribute xml:lang { xsd:language },
version,
locale-file.locale.info?,
(locale.style-options & locale.date+ & locale.terms)
}
locale.date =
element cs:date {
date.form,
delimiter,
font-formatting,
text-case,
locale.date.date-part+
}
date.form =
## Select the localized date format ("text" or "numeric").
attribute form {
## Text date form (e.g., "December 15, 2005").
"text"
|
## Numeric date form (e.g., "2005-12-15").
"numeric"
}
locale.date.date-part =
element cs:date-part {
affixes, font-formatting, text-case, (day | month | year)
}
locale.terms = element cs:terms { terms.term+ }
## The "cs:term" element can either hold a basic string, or "cs:single" and
## "cs:multiple" child elements to give singular and plural forms of the term.
terms.term =
element cs:term {
term.attributes,
(text | (term.single, term.multiple))
}
term.attributes =
(attribute name { terms },
[ a:defaultValue = "long" ] attribute form { term.form }?)
| (attribute name { terms.ordinals },
attribute form { "long" }?,
attribute gender-form { "masculine" | "feminine" }?,
attribute match {
"last-digit" | "last-two-digits" | "whole-number"
}?)
| (attribute name { terms.long-ordinals },
attribute form { "long" }?,
attribute gender-form { "masculine" | "feminine" })
| (attribute name { terms.gender-assignable },
attribute form { "long" }?,
attribute gender { "masculine" | "feminine" })
## "verb-short" reverts to "verb" if the "verb-short" form is not available.
## "symbol" reverts to "short" if the "symbol" form is not available.
## "verb" and "short" revert to "long" if the specified form is not available.
term.form = "long" | "verb" | "short" | "verb-short" | "symbol"
term.single =
## Singular version of the term.
element cs:single { text }
term.multiple =
## Plural version of the term.
element cs:multiple { text }
}
# ==============================================================================
## cs:macro
div {
style.macro =
## Use to create collections of (reusable) formatting instructions.
element cs:macro {
attribute name { xsd:NMTOKEN },
rendering-element+
}
}
# ==============================================================================
## Rendering Elements
div {
rendering-element =
rendering-element.names
| rendering-element.date
| rendering-element.label
| rendering-element.text
| rendering-element.number
| rendering-element.choose
| rendering-element.group
}
# ==============================================================================
## cs:citation and cs:bibliography
div {
style.citation =
## Use to describe the formatting of citations.
element cs:citation { citation.options, sort?, citation.layout }
style.bibliography =
## Use to describe the formatting of the bibliography.
element cs:bibliography {
bibliography.options, sort?, bibliography.layout
}
citation.layout =
element cs:layout {
affixes, delimiter, font-formatting, rendering-element+
}
bibliography.layout =
element cs:layout { affixes, font-formatting, rendering-element+ }
}
# ==============================================================================
## cs:names Rendering Element
div {
rendering-element.names =
element cs:names {
names.attributes,
((names.name?, names.et-al?) & names.label?),
names.substitute?
}
names.attributes =
attribute variable {
list { variables.names+ }
},
affixes,
## Specify the delimiter for name lists of name variables rendered by
## the same cs:names element.
delimiter,
display,
font-formatting
names.name =
element cs:name {
name.attributes,
## Select the "long" (first name + last name, for Western names),
## "short" (last name only, for Western names), or "count" name form
## (returning the number of names in the name variable, which can be
## useful for some sorting algorithms).
[ a:defaultValue = "long" ]
attribute form { "long" | "short" | "count" }?,
affixes,
## Set the delimiter for names in a name variable (e.g., ", " in
## "Doe, Smith")
[ a:defaultValue = ", " ] delimiter,
font-formatting,
name.name-part*
}
name.attributes =
## Use to separate the second-to-last and last name of a name list by
## the "and" term or ampersand.
attribute and {
## Use the "and" term (e.g., "Doe, Johnson and Smith").
"text"
|
## Use the "ampersand" (e.g., "Doe, Johnson & Smith").
"symbol"
}?,
## Specify when the name delimiter is used between a truncated name list
## and the "et-al" (or "and others") term in case of et-al abbreviation
## (e.g., "Smith, Doe et al." or "Smith, Doe, et al.").
[ a:defaultValue = "contextual" ]
attribute delimiter-precedes-et-al {
## The name delimiter is only used when the truncated name list
## consists of two or more names.
"contextual"
|
## The name delimiter is always used.
"always"
|
## The name delimiter is never used.
"never"
|
## The name delimiter is only used if the preceding name is inverted as
## a result of the "name-as-sort-order" attribute.
"after-inverted-name"
}?,
## Specify when the name delimiter is used between the second-to-last
## and last name of a non-truncated name list. Only has an effect when
## the "and" term or ampersand is used (e.g., "Doe and Smith" or "Doe,
## and Smith").
[ a:defaultValue = "contextual" ]
attribute delimiter-precedes-last {
## The name delimiter is only used when the name list consists of
## three or more names.
"contextual"
|
## The name delimiter is always used.
"always"
|
## The name delimiter is never used.
"never"
|
## The name delimiter is only used if the preceding name is inverted as
## a result of the "name-as-sort-order" attribute.
"after-inverted-name"
}?,
## Set the minimum number of names needed in a name variable to activate
## et-al abbreviation.
attribute et-al-min { xsd:integer }?,
## Set the number of names to render when et-al abbreviation is active.
attribute et-al-use-first { xsd:integer }?,
## As "et-al-min", but only affecting subsequent citations to an item.
attribute et-al-subsequent-min { xsd:integer }?,
## As "et-al-use-first", but only affecting subsequent citations to an
## item.
attribute et-al-subsequent-use-first { xsd:integer }?,
## If set to "true", the "et-al" (or "and others") term is replaced by
## an ellipsis followed by the last name of the name variable.
[ a:defaultValue = "false" ]
attribute et-al-use-last { xsd:boolean }?,
## If set to "false", names are not initialized and "initialize-with"
## only affects initials already present in the input data.
[ a:defaultValue = "true" ] attribute initialize { xsd:boolean }?,
## Activate initializing of given names. The attribute value is appended
## to each initial (e.g., with ". ", "Orson Welles" becomes "O. Welles").
attribute initialize-with { text }?,
## Specify whether (and which) names should be rendered in their sort
## order (e.g., "Doe, John" instead of "John Doe").
attribute name-as-sort-order {
## Render the first name of each name variable in sort order.
"first"
|
## Render all names in sort order.
"all"
}?,
## Sets the delimiter for name-parts that have switched positions as a
## result of "name-as-sort-order" (e.g., ", " in "Doe, John").
[ a:defaultValue = ", " ] attribute sort-separator { text }?
name.name-part =
## Use to format individual name parts (e.g., "Jane DOE").
element cs:name-part {
attribute name { "family" | "given" },
affixes,
font-formatting,
text-case
}
names.et-al =
## Specify the term used for et-al abbreviation and its formatting.
element cs:et-al {
## Select the term to use for et-al abbreviation.
[ a:defaultValue = "et-al" ]
attribute term { "et-al" | "and others" }?,
font-formatting
}
## Inherits variable from the parent cs:names element.
names.label =
element cs:label {
[ a:defaultValue = "long" ] attribute form { term.form }?,
label.attributes-shared
}
names.substitute =
## Specify substitution options when the name variables selected on the
## parent cs:names element are empty.
element cs:substitute { (substitute.names | rendering-element)+ }
## Short version of cs:names, without children, allowed in cs:substitute.
substitute.names = element cs:names { names.attributes }
}
# ==============================================================================
## cs:date Rendering Element
div {
rendering-element.date =
element cs:date {
attribute variable { variables.dates },
((
## Limit the date parts rendered.
[ a:defaultValue = "year-month-day" ]
attribute date-parts {
## Year, month and day
"year-month-day"
|
## Year and month
"year-month"
|
## Year only
"year"
}?,
date.form,
rendering-element.date.date-part.localized*)
| (rendering-element.date.date-part.non-localized+, delimiter)),
affixes,
display,
font-formatting,
text-case
}
rendering-element.date.date-part.localized =
## Specify overriding formatting for localized dates (affixes
## cannot be overridden, as these are considered locale-specific).
## Example uses are forcing the use of leading-zeros, or of the
## "short" month form. Has no effect on which, and in what order,
## date parts are rendered.
element cs:date-part {
font-formatting, text-case, (day | month | year)
}
rendering-element.date.date-part.non-localized =
## Specify, in the desired order, the date parts that should be
## rendered and their formatting.
element cs:date-part {
affixes, font-formatting, text-case, (day | month | year)
}
day =
attribute name { "day" },
## Day forms: "numeric" ("5"), "numeric-leading-zeros" ("05"), "ordinal"
## ("5th").
[ a:defaultValue = "numeric" ]
attribute form { "numeric" | "numeric-leading-zeros" | "ordinal" }?,
range-delimiter
month =
attribute name { "month" },
## Months forms: "long" (e.g., "January"), "short" ("Jan."), "numeric"
## ("1"), and "numeric-leading-zeros" ("01").
[ a:defaultValue = "long" ]
attribute form {
"long" | "short" | "numeric" | "numeric-leading-zeros"
}?,
range-delimiter,
strip-periods
year =
attribute name { "year" },
## Year forms: "long" ("2005"), "short" ("05").
[ a:defaultValue = "long" ] attribute form { "short" | "long" }?,
range-delimiter
range-delimiter =
## Specify a delimiter for date ranges (by default the en-dash). A custom
## delimiter is retrieved from the largest date part ("day", "month" or
## "year") that differs between the two dates.
[ a:defaultValue = "–" ] attribute range-delimiter { text }?
}
# ==============================================================================
## cs:text Rendering Element
div {
rendering-element.text =
## Use to call macros, render variables, terms, or verbatim text.
element cs:text {
text.attributes,
affixes,
display,
font-formatting,
quotes,
strip-periods,
text-case
}
text.attributes =
## Select a macro.
attribute macro { xsd:NMTOKEN }
| (
## Select a term.
attribute term { terms },
[ a:defaultValue = "long" ] attribute form { term.form }?,
## Specify term plurality: singular ("false") or plural ("true").
[ a:defaultValue = "false" ] attribute plural { xsd:boolean }?)
|
## Specify verbatim text.
attribute value { text }
| (
## Select a variable.
attribute variable { variables.standard },
[ a:defaultValue = "long" ] attribute form { "short" | "long" }?)
}
# ==============================================================================
## cs:number Rendering Element
div {
rendering-element.number =
## Use to render a number variable.
element cs:number {
number.attributes, affixes, display, font-formatting, text-case
}
number.attributes =
attribute variable { variables.numbers },
## Number forms: "numeric" ("4"), "ordinal" ("4th"), "long-ordinal"
## ("fourth"), "roman" ("iv").
[ a:defaultValue = "numeric" ]
attribute form { "numeric" | "ordinal" | "long-ordinal" | "roman" }?
}
# ==============================================================================
## cs:label Rendering Element
div {
rendering-element.label =
## Use to render a term whose pluralization depends on the content of a
## variable. E.g., if "page" variable holds a range, the plural label
## "pp." is selected instead of the singular "p.".
element cs:label { label.attributes, label.attributes-shared }
label.attributes =
attribute variable { variables.numbers | "locator" | "page" },
[ a:defaultValue = "long" ]
attribute form { "long" | "short" | "symbol" }?
label.attributes-shared =
## Specify when the plural version of a term is selected.
[ a:defaultValue = "contextual" ]
attribute plural { "always" | "never" | "contextual" }?,
affixes,
font-formatting,
strip-periods,
text-case
}
# ==============================================================================
## cs:group Rendering Element
div {
rendering-element.group =
## Use to group rendering elements. Groups are useful for setting a
## delimiter for the group children, for organizing the layout of
## bibliographic entries (using the "display" attribute), and for
## suppressing the rendering of terms and verbatim text when variables
## are empty.
element cs:group {
group.attributes,
affixes,
delimiter,
display,
font-formatting,
rendering-element+
}
group.attributes = notAllowed?
}
# ==============================================================================
## Style Options
div {
style.options =
style.demote-non-dropping-particle,
style.initialize-with-hyphen,
style.page-range-format,
names-inheritable-options,
name-inheritable-options
citation.options =
citation.cite-group-delimiter,
citation.collapse-options,
citation.disambiguate-options,
citation.near-note-distance,
names-inheritable-options,
name-inheritable-options
bibliography.options =
bibliography.hanging-indent,
bibliography.line-formatting-options,
bibliography.second-field-align,
bibliography.subsequent-author-substitute-options,
names-inheritable-options,
name-inheritable-options
style.demote-non-dropping-particle =
## Specify whether the non-dropping particle is demoted in inverted
## names (e.g., "Koning, W. de").
[ a:defaultValue = "display-and-sort" ]
attribute demote-non-dropping-particle {
"never" | "sort-only" | "display-and-sort"
}?
style.initialize-with-hyphen =
## Specify whether compound given names (e.g., "Jean-Luc") are
## initialized with ("J-L") or without a hyphen ("JL").
[ a:defaultValue = "true" ]
attribute initialize-with-hyphen { xsd:boolean }?
style.page-range-format =
## Reformat page ranges in the "page" variable.
attribute page-range-format {
"expanded"
| "minimal"
| "minimal-two"
| "chicago"
| "chicago-15"
| "chicago-16"
}?
citation.cite-group-delimiter =
## Activate cite grouping and specify the delimiter for cites within a
## cite group.
[ a:defaultValue = ", " ] attribute cite-group-delimiter { text }?
citation.collapse-options =
## Activate cite grouping and specify the method of citation collapsing.
attribute collapse {
## Collapse ranges of numeric cites, e.g. from "[1,2,3]" to "[1-3]".
"citation-number"
|
## Collapse cites by suppressing repeated names, e.g. from "(Doe
## 2000, Doe 2001)" to "(Doe 2000, 2001)".
"year"
|
## Collapse cites as with "year", but also suppresses repeated
## years, e.g. from "(Doe 2000a, Doe 2000b)" to "(Doe 2000a, b)".
"year-suffix"
|
## Collapses cites as with "year-suffix", but also collapses
## ranges of year-suffixes, e.g. from "(Doe 2000a, Doe 2000b,
## Doe 2000c)" to "(Doe 2000a-c)".
"year-suffix-ranged"
}?,
## Specify the delimiter between year-suffixes. Defaults to the cite
## delimiter.
attribute year-suffix-delimiter { text }?,
## Specify the delimiter following a group of collapsed cites. Defaults
## to the cite delimiter.
attribute after-collapse-delimiter { text }?
citation.disambiguate-options =
## Set to "true" to activate disambiguation by showing names that were
## originally hidden as a result of et-al abbreviation.
[ a:defaultValue = "false" ]
attribute disambiguate-add-names { xsd:boolean }?,
## Set to "true" to activate disambiguation by expanding names, showing
## initials or full given names.
[ a:defaultValue = "false" ]
attribute disambiguate-add-givenname { xsd:boolean }?,
## Set to "true" to activate disambiguation by adding year-suffixes
## (e.g., "(Doe 2007a, Doe 2007b)") for items from the same author(s)
## and year.
[ a:defaultValue = "false" ]
attribute disambiguate-add-year-suffix { xsd:boolean }?,
## Specify how name are expanded for disambiguation.
[ a:defaultValue = "by-cite" ]
attribute givenname-disambiguation-rule {
## Each ambiguous names is progressively transformed until
## disambiguated (when disambiguation is not possible, the name
## remains in its original form).
"all-names"
|
## As "all-names", but name expansion is limited to showing
## initials.
"all-names-with-initials"
|
## As "all-names", but disambiguation is limited to the first name
## of each cite.
"primary-name"
|
## As "all-names-with-initials", but disambiguation is limited to
## the first name of each cite.
"primary-name-with-initials"
|
## As "all-names", but only ambiguous names in ambiguous cites are
## expanded.
"by-cite"
}?
citation.near-note-distance =
## Set the number of preceding notes (footnotes or endnotes) within
## which the current item needs to have been previously cited in order
## for the "near-note" position to be "true".
[ a:defaultValue = "5" ]
attribute near-note-distance { xsd:integer }?
bibliography.hanging-indent =
## Set to "true" to render bibliographic entries with hanging indents.
[ a:defaultValue = "false" ]
attribute hanging-indent { xsd:boolean }?
bibliography.line-formatting-options =
## Set the spacing between bibliographic entries.
[ a:defaultValue = "1" ]
attribute entry-spacing { xsd:nonNegativeInteger }?,
## Set the spacing between bibliographic lines.
[ a:defaultValue = "1" ]
attribute line-spacing {
xsd:integer { minExclusive = "0" }
}?
bibliography.second-field-align =
## Use to align any subsequent lines of bibliographic entries with the
## beginning of the second field.
attribute second-field-align {
## Align the first field with the margin.
"flush"
|
## Put the first field in the margin and align all subsequent
## lines of text with the margin.
"margin"
}?
bibliography.subsequent-author-substitute-options =
## Substitute names that repeat in subsequent bibliographic entries by
## the attribute value.
attribute subsequent-author-substitute { text }?,
## Specify the method of substitution of names repeated in subsequent
## bibliographic entries.
[ a:defaultValue = "complete-all" ]
attribute subsequent-author-substitute-rule {
## Requires a match of all rendered names in the name variable, and
## substitutes once for all names.
"complete-all"
|
## Requires a match of all rendered names in the name variable,
## and substitutes for each name.
"complete-each"
|
## Substitutes for each name, until the first mismatch.
"partial-each"
|
## Substitutes the first name if it matches.
"partial-first"
}?
## Options affecting cs:names, for cs:style, cs:citation and cs:bibliography.
names-inheritable-options =
## Inheritable name option, companion for "delimiter" on cs:names.
attribute names-delimiter { text }?
## Options affecting cs:name, for cs:style, cs:citation and cs:bibliography.
name-inheritable-options =
name.attributes,
## Inheritable name option, companion for "delimiter" on cs:name.
attribute name-delimiter { text }?,
## Inheritable name option, companion for "form" on cs:name.
[ a:defaultValue = "long" ]
attribute name-form { "long" | "short" | "count" }?
}
# ==============================================================================
## cs:sort - Sorting
div {
sort =
## Specify how cites and bibliographic entries should be sorted. By
## default, items appear in the order in which they were cited.
element cs:sort { sort.key+ }
sort.key =
element cs:key {
(attribute variable { variables }
| attribute macro { xsd:NMTOKEN }),
## The minimum number of names needed in a name variable to activate
## name list truncation. Overrides the values set on any
## "et-al-(subsequent-)min" attributes.
attribute names-min { xsd:integer }?,
## The number of names to render when name list truncation is
## activated. Overrides the values set on the
## "et-al-(subsequent-)use-first" attributes.
attribute names-use-first { xsd:integer }?,
## Use to override the value of the "et-at-use-last" attribute.
attribute names-use-last { xsd:boolean }?,
## Select between an ascending and descending sort.
[ a:defaultValue = "ascending" ]
attribute sort { "ascending" | "descending" }?
}
}
# ==============================================================================
## Formatting attributes.
div {
affixes =
[ a:defaultValue = "" ] attribute prefix { text }?,
[ a:defaultValue = "" ] attribute suffix { text }?
delimiter = attribute delimiter { text }?
display =
## By default, bibliographic entries consist of continuous runs of text.
## With the "display" attribute, portions of each entry can be
## individually positioned.
attribute display {
## Places the content in a block stretching from margin to margin.
"block"
|
## Places the content in a block starting at the left margin.
"left-margin"
|
## Places the content in a block to the right of a preceding
## "left-margin" block.
"right-inline"
|
## Places the content in a block indented to the right by a standard
## amount.
"indent"
}?
## The font-formatting attributes are based on those of CSS and XSL-FO.
font-formatting =
[ a:defaultValue = "normal" ]
attribute font-style { "italic" | "normal" | "oblique" }?,
[ a:defaultValue = "normal" ]
attribute font-variant { "normal" | "small-caps" }?,
[ a:defaultValue = "normal" ]
attribute font-weight { "normal" | "bold" | "light" }?,
[ a:defaultValue = "none" ]
attribute text-decoration { "none" | "underline" }?,
[ a:defaultValue = "baseline" ]
attribute vertical-align { "baseline" | "sup" | "sub" }?
quotes =
## When set to "true", quotes are placed around the rendered text.
[ a:defaultValue = "false" ] attribute quotes { xsd:boolean }?
strip-periods =
## When set to "true", periods are removed from the rendered text.
[ a:defaultValue = "false" ]
attribute strip-periods { xsd:boolean }?
text-case =
attribute text-case {
## Renders text in lowercase.
"lowercase"
|
## Renders text in uppercase.
"uppercase"
|
## Capitalizes the first character (other characters remain in
## their original case).
"capitalize-first"
|
## Capitalizes the first character of every word (other characters
## remain in their original case).
"capitalize-all"
|
## Renders text in title case.
"title"
|
## Renders text in sentence case.
## Deprecated. Will be removed in CSL 1.1
"sentence"
}?
}
[
{
"id": "zhang2024novel",
"type": "article-journal",
"title": "A novel approach to deep reinforcement learning for autonomous systems",
"author": [
{"family": "Zhang", "given": "Wei"},
{"family": "Li", "given": "Ming"},
{"family": "Wang", "given": "Fang"},
{"family": "Chen", "given": "Xiaohui"}
],
"container-title": "IEEE Transactions on Neural Networks and Learning Systems",
"volume": "15",
"issue": "3",
"page": "245-260",
"issued": {"date-parts": [[2024]]},
"DOI": "10.1109/TNNLS.2024.0012345"
},
{
"id": "smith2023intro",
"type": "book",
"title": "Introduction to Machine Learning: Theory and Applications",
"author": [
{"family": "Smith", "given": "John"}
],
"edition": "3",
"publisher": "Academic Press",
"publisher-place": "New York",
"issued": {"date-parts": [[2023]]},
"ISBN": "978-0-12-345678-9"
},
{
"id": "johnson2024transfer",
"type": "paper-conference",
"title": "Transfer learning with large-scale pre-trained models",
"author": [
{"family": "Johnson", "given": "Alice"}
],
"container-title": "Proceedings of the 41st International Conference on Machine Learning (ICML 2024)",
"publisher-place": "Vienna, Austria",
"page": "112-120",
"issued": {"date-parts": [[2024]]}
},
{
"id": "brown2023ethical",
"type": "chapter",
"title": "Ethical considerations in artificial intelligence deployment",
"author": [
{"family": "Brown", "given": "David"}
],
"container-title": "Handbook of Artificial Intelligence",
"publisher": "Springer",
"publisher-place": "Berlin",
"page": "45-78",
"issued": {"date-parts": [[2023]]}
},
{
"id": "who2024global",
"type": "webpage",
"title": "Global health report 2024",
"author": [
{"literal": "World Health Organization"}
],
"URL": "https://www.who.int/report2024",
"issued": {"date-parts": [[2024]]},
"accessed": {"date-parts": [[2024, 6, 15]]}
},
{
"id": "lee2023ml",
"type": "thesis",
"title": "Machine Learning in Healthcare: Predictive Models for Clinical Decision Support",
"author": [
{"family": "Lee", "given": "Sarah"}
],
"publisher": "Massachusetts Institute of Technology",
"issued": {"date-parts": [[2023]]},
"genre": "Ph.D. Dissertation"
},
{
"id": "garcia2024ai",
"type": "article-newspaper",
"title": "AI Breakthrough Announced: New Model Surpasses Human Performance",
"author": [
{"family": "Garcia", "given": "Maria"}
],
"container-title": "The New York Times",
"issued": {"date-parts": [[2024, 3, 15]]}
},
{
"id": "noaa2024climate",
"type": "report",
"title": "Annual Climate Assessment 2024",
"author": [
{"literal": "National Oceanic and Atmospheric Administration"}
],
"publisher": "NOAA",
"publisher-place": "Washington, DC",
"issued": {"date-parts": [[2024]]}
},
{
"id": "zhang2024zhongyao",
"type": "article-journal",
"title": "基于深度学习的中文文本情感分析研究",
"author": [
{"family": "张", "given": "伟"},
{"family": "李", "given": "娜"},
{"family": "王", "given": "强"}
],
"container-title": "计算机学报",
"volume": "47",
"issue": "5",
"page": "1023-1035",
"issued": {"date-parts": [[2024]]},
"language": "zh-CN"
},
{
"id": "liu2023rengong",
"type": "book",
"title": "人工智能导论",
"author": [
{"family": "刘", "given": "明"},
{"family": "陈", "given": "静"}
],
"publisher": "清华大学出版社",
"publisher-place": "北京",
"issued": {"date-parts": [[2023]]},
"language": "zh-CN"
}
]
#!/usr/bin/env python3
"""CSL file validator with three validation stages.
Usage:
python validate_csl.py <file.csl>
python validate_csl.py --verbose <file.csl>
Stages:
1. XML Syntax check (well-formed, UTF-8)
2. CSL RelaxNG Schema validation (downloads & caches schema)
3. Logic rules check (R1-R6)
"""
import argparse
import json
import os
import re
import sys
import urllib.request
import urllib.error
from pathlib import Path
from lxml import etree
# CSL namespace
CSL_NS = "http://purl.org/net/xbiblio/csl"
NS = {"csl": CSL_NS}
# Schema download URLs (raw GitHub content)
# Use v1.0.2 tag for CSL 1.0 files (the vast majority of existing styles)
# Note: The official schema doesn't fully cover all CSL 1.0.2 features
# (e.g., multi-layout with locale attributes). Schema errors for such
# features are expected and should be interpreted with this in mind.
SCHEMA_BRANCHES = {
"1.0": "v1.0.2",
"1.1": "master", # master tracks the upcoming 1.1 spec
}
SCHEMA_BASE_URL_TEMPLATE = "https://raw.githubusercontent.com/citation-style-language/schema/{branch}/schemas/styles"
SCHEMA_FILES = [
"csl.rnc",
"csl-categories.rnc",
"csl-choose.rnc",
"csl-terms.rnc",
"csl-types.rnc",
"csl-variables.rnc",
]
SCRIPT_DIR = Path(__file__).resolve().parent
SCHEMA_DIR = SCRIPT_DIR / "schema"
def log_verbose(msg, verbose=False):
if verbose:
print(f" [verbose] {msg}", file=sys.stderr)
# ---------------------------------------------------------------------------
# Stage 1: XML Syntax Check
# ---------------------------------------------------------------------------
def stage1_xml_syntax(filepath, verbose=False):
"""Check well-formed XML and UTF-8 encoding."""
errors = []
# Check encoding declaration
try:
with open(filepath, "rb") as f:
raw = f.read(200)
# Try to detect encoding from XML declaration
header = raw.decode("ascii", errors="replace")
if "encoding=" in header.lower():
match = re.search(r'encoding=["\']([^"\']+)["\']', header, re.IGNORECASE)
if match:
declared = match.group(1).lower().replace("-", "")
if declared not in ("utf8",):
errors.append({
"rule": "XML",
"message": f"Encoding declared as '{match.group(1)}', expected 'utf-8'",
"severity": "error",
})
# Try parsing as UTF-8
with open(filepath, "r", encoding="utf-8") as f:
f.read()
except UnicodeDecodeError as e:
errors.append({
"rule": "XML",
"message": f"File is not valid UTF-8: {e}",
"severity": "error",
})
# Parse XML
try:
parser = etree.XMLParser(recover=False)
tree = etree.parse(filepath, parser)
log_verbose("XML parsed successfully", verbose)
except etree.XMLSyntaxError as e:
errors.append({
"rule": "XML",
"message": f"XML syntax error: {e}",
"severity": "error",
})
return {"stage": 1, "name": "XML Syntax", "passed": len(errors) == 0, "errors": errors}, None
passed = len(errors) == 0
return {"stage": 1, "name": "XML Syntax", "passed": passed, "errors": errors}, tree
# ---------------------------------------------------------------------------
# Stage 2: CSL Schema Validation (RelaxNG)
# ---------------------------------------------------------------------------
def _get_schema_dir(version):
"""Return version-specific schema cache directory."""
branch = SCHEMA_BRANCHES.get(version, SCHEMA_BRANCHES["1.0"])
return SCHEMA_DIR / branch
def download_schema_files(version="1.0", verbose=False):
"""Download CSL .rnc schema files and cache them locally."""
branch = SCHEMA_BRANCHES.get(version, SCHEMA_BRANCHES["1.0"])
base_url = SCHEMA_BASE_URL_TEMPLATE.format(branch=branch)
schema_dir = _get_schema_dir(version)
schema_dir.mkdir(parents=True, exist_ok=True)
log_verbose(f"Schema branch: {branch} (CSL version {version})", verbose)
for fname in SCHEMA_FILES:
target = schema_dir / fname
if target.exists():
log_verbose(f"Schema file cached: {fname}", verbose)
continue
url = f"{base_url}/{fname}"
log_verbose(f"Downloading {url}", verbose)
try:
req = urllib.request.Request(url, headers={"User-Agent": "CSL-Validator/1.0"})
with urllib.request.urlopen(req, timeout=30) as resp:
data = resp.read()
target.write_bytes(data)
log_verbose(f"Saved {fname} ({len(data)} bytes)", verbose)
except (urllib.error.URLError, urllib.error.HTTPError, OSError) as e:
raise RuntimeError(f"Failed to download {fname}: {e}")
def convert_rnc_to_rng(version="1.0", verbose=False):
"""Convert csl.rnc to csl.rng using rnc2rng library. Returns path or None."""
schema_dir = _get_schema_dir(version)
rng_path = schema_dir / "csl.rng"
rnc_path = schema_dir / "csl.rnc"
if rng_path.exists():
log_verbose("Using cached csl.rng", verbose)
return rng_path
try:
import rnc2rng
except ImportError:
return None
log_verbose("Converting csl.rnc -> csl.rng via rnc2rng", verbose)
try:
# rnc2rng.load() resolves include directives relative to cwd,
# so we need to change to the schema directory temporarily.
old_cwd = os.getcwd()
os.chdir(str(schema_dir))
try:
tree = rnc2rng.load(str(rnc_path))
rng_xml = rnc2rng.dumps(tree)
finally:
os.chdir(old_cwd)
rng_path.write_text(rng_xml, encoding="utf-8")
log_verbose("Conversion successful", verbose)
return rng_path
except Exception as e:
log_verbose(f"rnc2rng conversion failed: {e}", verbose)
return None
def stage2_schema_validation(tree, verbose=False):
"""Validate against CSL RelaxNG schema."""
if tree is None:
return {
"stage": 2, "name": "CSL Schema", "passed": False,
"errors": [{"rule": "Schema", "message": "Skipped: XML parse failed in Stage 1", "severity": "error"}],
"skipped": True,
}
# Detect CSL version from the root element
root = tree.getroot()
csl_version = root.get("version", "1.0")
# Normalize: "1.0" stays "1.0", "1.1" stays "1.1", anything else defaults to "1.0"
if csl_version not in SCHEMA_BRANCHES:
log_verbose(f"Unknown CSL version '{csl_version}', defaulting to 1.0 schema", verbose)
csl_version = "1.0"
# Download schema files
try:
download_schema_files(version=csl_version, verbose=verbose)
except RuntimeError as e:
msg = f"Schema download failed: {e}"
log_verbose(msg, verbose)
print(f" WARNING: {msg}", file=sys.stderr)
return {
"stage": 2, "name": "CSL Schema", "passed": False,
"errors": [{"rule": "Schema", "message": msg, "severity": "warning"}],
"skipped": True,
}
# Convert .rnc to .rng
rng_path = convert_rnc_to_rng(version=csl_version, verbose=verbose)
if rng_path is None:
msg = "rnc2rng library not available; cannot convert .rnc to .rng. Install with: pip install rnc2rng"
log_verbose(msg, verbose)
print(f" WARNING: {msg}", file=sys.stderr)
return {
"stage": 2, "name": "CSL Schema", "passed": False,
"errors": [{"rule": "Schema", "message": msg, "severity": "warning"}],
"skipped": True,
}
# Validate
errors = []
try:
rng_doc = etree.parse(str(rng_path))
rng_schema = etree.RelaxNG(rng_doc)
valid = rng_schema.validate(tree)
if not valid:
for err in rng_schema.error_log:
errors.append({
"rule": "Schema",
"message": str(err),
"severity": "error",
})
log_verbose(f"Schema validation: {'PASS' if valid else 'FAIL'}", verbose)
except etree.RelaxNGParseError as e:
msg = f"Failed to parse RelaxNG schema: {e}"
log_verbose(msg, verbose)
print(f" WARNING: {msg}", file=sys.stderr)
return {
"stage": 2, "name": "CSL Schema", "passed": False,
"errors": [{"rule": "Schema", "message": msg, "severity": "warning"}],
"skipped": True,
}
passed = len(errors) == 0
return {"stage": 2, "name": "CSL Schema", "passed": passed, "errors": errors, "skipped": False}
# ---------------------------------------------------------------------------
# Stage 3: Logic Rules
# ---------------------------------------------------------------------------
def stage3_logic_rules(tree, verbose=False):
"""Custom logic rules R1-R6."""
if tree is None:
return {
"stage": 3, "name": "Logic Rules", "passed": False,
"errors": [{"rule": "R0", "message": "Skipped: XML parse failed in Stage 1", "severity": "error"}],
}
root = tree.getroot()
errors = []
# ---- R1: Structure completeness ----
_check_r1(root, errors, verbose)
# ---- R2: Macro reference integrity ----
_check_r2(root, errors, verbose)
# ---- R3: Class consistency ----
_check_r3(root, errors, verbose)
# ---- R4: et-al parameter validity ----
_check_r4(root, errors, verbose)
# ---- R5: Bilingual layout ordering ----
_check_r5(root, errors, verbose)
# ---- R6: No residual placeholders or empty macros ----
_check_r6(root, errors, verbose)
has_error = any(e["severity"] == "error" for e in errors)
return {"stage": 3, "name": "Logic Rules", "passed": not has_error, "errors": errors}
def _check_r1(root, errors, verbose):
"""R1: Structural completeness."""
# style must have class and version
style_class = root.get("class")
style_version = root.get("version")
if not style_class:
errors.append({"rule": "R1", "message": "<style> missing 'class' attribute", "severity": "error"})
if not style_version:
errors.append({"rule": "R1", "message": "<style> missing 'version' attribute", "severity": "error"})
# info must have title, id, updated
info = root.find("csl:info", NS)
if info is None:
errors.append({"rule": "R1", "message": "<info> element not found", "severity": "error"})
else:
for tag in ("title", "id", "updated"):
el = info.find(f"csl:{tag}", NS)
if el is None or not (el.text and el.text.strip()):
errors.append({"rule": "R1", "message": f"<info> missing or empty <{tag}>", "severity": "error"})
# citation must exist with layout
citation = root.find("csl:citation", NS)
if citation is None:
errors.append({"rule": "R1", "message": "<citation> element not found", "severity": "error"})
else:
layouts = citation.findall("csl:layout", NS)
if not layouts:
errors.append({"rule": "R1", "message": "<citation> has no <layout> child", "severity": "error"})
log_verbose(f"R1: {len([e for e in errors if e['rule'] == 'R1'])} issues", verbose)
def _check_r2(root, errors, verbose):
"""R2: Macro reference integrity."""
# Collect defined macros
defined = set()
for macro in root.findall("csl:macro", NS):
name = macro.get("name")
if name:
defined.add(name)
# Collect referenced macros (text[@macro] anywhere in the tree)
referenced = set()
for el in root.iter(f"{{{CSL_NS}}}text"):
macro_ref = el.get("macro")
if macro_ref:
referenced.add(macro_ref)
# Also check <names> with <substitute> -> <names> pattern is ok, but
# text[@macro] in any element
for el in root.iter():
macro_ref = el.get("macro")
if macro_ref and el.tag == f"{{{CSL_NS}}}text":
referenced.add(macro_ref)
# Referenced but not defined -> error
for name in sorted(referenced - defined):
errors.append({
"rule": "R2",
"message": f"Macro '{name}' referenced but not defined",
"severity": "error",
})
# Defined but never referenced -> warning
for name in sorted(defined - referenced):
errors.append({
"rule": "R2",
"message": f"Macro '{name}' defined but never referenced",
"severity": "warning",
})
log_verbose(f"R2: defined={len(defined)}, referenced={len(referenced)}", verbose)
def _check_r3(root, errors, verbose):
"""R3: Class consistency — citation-format should match class."""
style_class = root.get("class")
if not style_class:
return
# Find citation-format from <category citation-format="..."/>
citation_format = None
for cat in root.iter(f"{{{CSL_NS}}}category"):
cf = cat.get("citation-format")
if cf:
citation_format = cf
break
if citation_format is None:
log_verbose("R3: No citation-format category found, skipping", verbose)
return
if style_class == "in-text":
valid_formats = ("numeric", "author-date", "author", "label")
if citation_format not in valid_formats:
errors.append({
"rule": "R3",
"message": f"class='in-text' but citation-format='{citation_format}' "
f"(expected one of: {', '.join(valid_formats)})",
"severity": "error",
})
elif style_class == "note":
if citation_format != "note":
errors.append({
"rule": "R3",
"message": f"class='note' but citation-format='{citation_format}' (expected 'note')",
"severity": "error",
})
log_verbose(f"R3: class={style_class}, citation-format={citation_format}", verbose)
def _check_r4(root, errors, verbose):
"""R4: et-al-min > et-al-use-first for all <name> elements and inherited attributes."""
count = 0
# Gather elements that can carry et-al-min / et-al-use-first:
# <name>, <style>, <citation>, <bibliography>
targets = []
for tag in ("name", "style", "citation", "bibliography"):
for el in root.iter(f"{{{CSL_NS}}}{tag}"):
targets.append((tag, el))
for tag, el in targets:
ea_min_str = el.get("et-al-min")
ea_first_str = el.get("et-al-use-first")
if ea_min_str is not None and ea_first_str is not None:
try:
ea_min = int(ea_min_str)
ea_first = int(ea_first_str)
except ValueError:
errors.append({
"rule": "R4",
"message": f"<{tag}> has non-integer et-al-min='{ea_min_str}' or et-al-use-first='{ea_first_str}'",
"severity": "error",
})
continue
if ea_min <= ea_first:
errors.append({
"rule": "R4",
"message": f"<{tag}> has et-al-min={ea_min} <= et-al-use-first={ea_first} "
f"(et-al-min must be greater)",
"severity": "error",
})
count += 1
log_verbose(f"R4: checked {count} elements with et-al attributes", verbose)
def _check_r5(root, errors, verbose):
"""R5: Bilingual layout ordering — layouts with locale attribute should come before those without."""
for parent_tag in ("citation", "bibliography"):
parent = root.find(f"csl:{parent_tag}", NS)
if parent is None:
continue
layouts = parent.findall("csl:layout", NS)
if len(layouts) <= 1:
continue
# Check ordering: locale-specific layouts should precede the generic one
found_generic = False
for layout in layouts:
has_locale = layout.get("locale") is not None
if not has_locale:
found_generic = True
elif found_generic:
# A locale-specific layout appears after a generic one
locale_val = layout.get("locale", "")
errors.append({
"rule": "R5",
"message": f"In <{parent_tag}>: layout with locale='{locale_val}' "
f"appears after a layout without locale attribute "
f"(locale-specific layouts should come first)",
"severity": "warning",
})
log_verbose("R5: layout ordering checked", verbose)
def _check_r6(root, errors, verbose):
"""R6: No residual placeholders or empty macros."""
# Check for placeholder text patterns like [占位符], [placeholder], [TODO], etc.
placeholder_pattern = re.compile(r"\[.*?占位.*?\]|\[placeholder\]|\[TODO\]|\[FIXME\]|\[TBD\]", re.IGNORECASE)
for el in root.iter():
if not isinstance(el.tag, str):
continue # skip comments and processing instructions
tag_local = etree.QName(el.tag).localname
# Check text content
if el.text:
matches = placeholder_pattern.findall(el.text)
for m in matches:
errors.append({
"rule": "R6",
"message": f"Residual placeholder text found: '{m}' in <{tag_local}>",
"severity": "error",
})
# Check attribute values
for attr_name, attr_val in el.attrib.items():
matches = placeholder_pattern.findall(attr_val)
for m in matches:
errors.append({
"rule": "R6",
"message": f"Residual placeholder in attribute {attr_name}='{attr_val}' of <{tag_local}>",
"severity": "error",
})
# Check tail text
if el.tail:
matches = placeholder_pattern.findall(el.tail)
for m in matches:
errors.append({
"rule": "R6",
"message": f"Residual placeholder text found in tail: '{m}'",
"severity": "error",
})
# Check for empty macros (macros with no child elements)
for macro in root.findall("csl:macro", NS):
name = macro.get("name", "(unnamed)")
if len(macro) == 0:
# No child elements at all
errors.append({
"rule": "R6",
"message": f"Empty macro '{name}' (no child elements)",
"severity": "error",
})
log_verbose("R6: placeholder and empty macro check done", verbose)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def validate_csl(filepath, verbose=False):
"""Run all validation stages and return the result dict."""
filepath = str(Path(filepath).resolve())
if not os.path.isfile(filepath):
return {
"file": filepath,
"stages": [],
"overall": "FAIL",
"error": f"File not found: {filepath}",
}
stages = []
# Stage 1
if verbose:
print("Stage 1: XML Syntax Check...", file=sys.stderr)
s1_result, tree = stage1_xml_syntax(filepath, verbose)
stages.append(s1_result)
# Stage 2
if verbose:
print("Stage 2: CSL Schema Validation...", file=sys.stderr)
s2_result = stage2_schema_validation(tree, verbose)
stages.append(s2_result)
# Stage 3
if verbose:
print("Stage 3: Logic Rules Check...", file=sys.stderr)
s3_result = stage3_logic_rules(tree, verbose)
stages.append(s3_result)
# Determine overall result
all_errors = []
for s in stages:
all_errors.extend(s.get("errors", []))
has_error = any(e["severity"] == "error" for e in all_errors) # noqa: F841
has_warning = any(e["severity"] == "warning" for e in all_errors) # noqa: F841
# Skipped stages don't count as errors for overall if they only have warnings
skipped_only_warnings = True
for s in stages:
if s.get("skipped"):
if any(e["severity"] == "error" for e in s.get("errors", [])):
skipped_only_warnings = False # noqa: F841
# Recalculate has_error excluding skipped-stage warnings
real_errors = []
for s in stages:
if s.get("skipped"):
continue
real_errors.extend(e for e in s.get("errors", []) if e["severity"] == "error")
real_warnings = []
for s in stages:
if s.get("skipped"):
# Skipped stage warnings are informational
real_warnings.extend(e for e in s.get("errors", []) if e["severity"] == "warning")
else:
real_warnings.extend(e for e in s.get("errors", []) if e["severity"] == "warning")
if real_errors:
overall = "FAIL"
elif real_warnings:
overall = "WARN"
else:
overall = "PASS"
return {
"file": filepath,
"stages": stages,
"overall": overall,
}
def main():
parser = argparse.ArgumentParser(
description="Validate a CSL (Citation Style Language) file.",
epilog="Example: python validate_csl.py --verbose style.csl",
)
parser.add_argument("file", help="Path to the CSL file to validate")
parser.add_argument("--verbose", "-v", action="store_true",
help="Show detailed progress on stderr")
args = parser.parse_args()
result = validate_csl(args.file, verbose=args.verbose)
print(json.dumps(result, ensure_ascii=False, indent=2))
# Exit code
if result["overall"] == "FAIL":
sys.exit(1)
elif result["overall"] == "WARN":
sys.exit(0)
else:
sys.exit(0)
if __name__ == "__main__":
main()