diff --git a/.github/workflows/check-format-and-test-python-markdown-extension.yml b/.github/workflows/check-format-and-test-python-markdown-extension.yml
new file mode 100644
index 00000000..df6737de
--- /dev/null
+++ b/.github/workflows/check-format-and-test-python-markdown-extension.yml
@@ -0,0 +1,46 @@
+name: Check PR Format and Test for python-markdown-extension
+
+on:
+ pull_request:
+ branches:
+ - master
+ paths:
+ - python-markdown-extension/**
+
+jobs:
+ check-format:
+ name: Check PR Format
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: ./python-markdown-extension
+ steps:
+ - uses: actions/checkout@v4
+ name: Checkout Repo
+ - uses: eifinger/setup-rye@v3
+ name: Setup Rye
+ with:
+ enable-cache: true
+ working-directory: python-markdown-extension
+ - run: rye sync
+ name: Install Dependencies
+ - run: rye fmt --check
+ name: Check Format
+ test:
+ name: Test PR
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: ./python-markdown-extension
+ steps:
+ - uses: actions/checkout@v4
+ name: Checkout Repo
+ - uses: eifinger/setup-rye@v3
+ name: Setup Rye
+ with:
+ enable-cache: true
+ working-directory: python-markdown-extension
+ - run: rye sync
+ name: Install Dependencies
+ - run: rye run test
+ name: Run Tests
diff --git a/.gitignore b/.gitignore
deleted file mode 100755
index 826b026a..00000000
--- a/.gitignore
+++ /dev/null
@@ -1,36 +0,0 @@
-public
-.cache
-node_modules
-*DS_Store
-*.env
-
-.idea/
-
-yarn-error.log
-.vscode
-
-__generated__/
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-pnpm-debug.log*
-lerna-debug.log*
-
-node_modules
-dist
-dist-ssr
-*.local
-
-# Editor directories and files
-.vscode/*
-!.vscode/extensions.json
-.idea
-.DS_Store
-*.suo
-*.ntvs*
-*.njsproj
-*.sln
-*.sw?
diff --git a/index.html b/index.html
deleted file mode 100644
index 44a93350..00000000
--- a/index.html
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
- Vite + TS
-
-
-
-
-
-
diff --git a/package.json b/package.json
deleted file mode 100644
index 07c6a13a..00000000
--- a/package.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "name": "feedback-sys",
- "private": true,
- "version": "0.0.0",
- "type": "module",
- "scripts": {
- "dev": "vite",
- "build": "tsc && vite build",
- "preview": "vite preview"
- },
- "devDependencies": {
- "typescript": "^5.2.2",
- "vite": "^5.2.0"
- }
-}
diff --git a/public/vite.svg b/public/vite.svg
deleted file mode 100644
index e7b8dfb1..00000000
--- a/public/vite.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/python-markdown-extension/.gitignore b/python-markdown-extension/.gitignore
new file mode 100644
index 00000000..bf07c5cb
--- /dev/null
+++ b/python-markdown-extension/.gitignore
@@ -0,0 +1,12 @@
+# python generated files
+__pycache__/
+*.py[oc]
+build/
+dist/
+wheels/
+*.egg-info
+
+# venv
+.venv
+
+.idea/
\ No newline at end of file
diff --git a/python-markdown-extension/.python-version b/python-markdown-extension/.python-version
new file mode 100644
index 00000000..871f80a3
--- /dev/null
+++ b/python-markdown-extension/.python-version
@@ -0,0 +1 @@
+3.12.3
diff --git a/python-markdown-extension/pyproject.toml b/python-markdown-extension/pyproject.toml
new file mode 100644
index 00000000..b641660a
--- /dev/null
+++ b/python-markdown-extension/pyproject.toml
@@ -0,0 +1,33 @@
+[project]
+name = "python_markdown_document_offsets_injection_extension"
+version = "0.0.1"
+description = "A Python-Markdown compiler plugin that put markdown words offset to the output HTML."
+authors = [{ name = "HikariLan", email = "hikarilan@minecraft.kim" }]
+license = { text = "Apache-2.0" }
+dependencies = [
+ "markdown>=3.6",
+]
+requires-python = ">= 3.8"
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.rye]
+managed = true
+dev-dependencies = [
+ "pygments>=2.18.0",
+ "pymdown-extensions>=10.8.1",
+]
+
+[tool.hatch.metadata]
+allow-direct-references = true
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/python_markdown_document_offsets_injection_extension"]
+
+[tool.rye.scripts]
+test = "python ./test"
+
+[project.entry-points."markdown.extensions"]
+document-offsets-injection = "python_markdown_document_offsets_injection_extension.extension:MainExtension"
diff --git a/python-markdown-extension/requirements-dev.lock b/python-markdown-extension/requirements-dev.lock
new file mode 100644
index 00000000..a33eba1b
--- /dev/null
+++ b/python-markdown-extension/requirements-dev.lock
@@ -0,0 +1,18 @@
+# generated by rye
+# use `rye lock` or `rye sync` to update this lockfile
+#
+# last locked with the following flags:
+# pre: false
+# features: []
+# all-features: false
+# with-sources: false
+# generate-hashes: false
+
+-e file:.
+markdown==3.6
+ # via pymdown-extensions
+ # via python-markdown-document-offsets-injection-extension
+pygments==2.18.0
+pymdown-extensions==10.8.1
+pyyaml==6.0.1
+ # via pymdown-extensions
diff --git a/python-markdown-extension/requirements.lock b/python-markdown-extension/requirements.lock
new file mode 100644
index 00000000..2e8b89cd
--- /dev/null
+++ b/python-markdown-extension/requirements.lock
@@ -0,0 +1,13 @@
+# generated by rye
+# use `rye lock` or `rye sync` to update this lockfile
+#
+# last locked with the following flags:
+# pre: false
+# features: []
+# all-features: false
+# with-sources: false
+# generate-hashes: false
+
+-e file:.
+markdown==3.6
+ # via python-markdown-document-offsets-injection-extension
diff --git a/python-markdown-extension/src/python_markdown_document_offsets_injection_extension/__init__.py b/python-markdown-extension/src/python_markdown_document_offsets_injection_extension/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/python-markdown-extension/src/python_markdown_document_offsets_injection_extension/extension.py b/python-markdown-extension/src/python_markdown_document_offsets_injection_extension/extension.py
new file mode 100644
index 00000000..a2aa3b5b
--- /dev/null
+++ b/python-markdown-extension/src/python_markdown_document_offsets_injection_extension/extension.py
@@ -0,0 +1,247 @@
+import re
+from markdown import Extension, Markdown
+from markdown.preprocessors import Preprocessor
+from markdown.blockprocessors import BlockProcessor
+from markdown.blockparser import BlockParser
+import xml.etree.ElementTree as etree
+
+MARK_PREVENT_RECURSION: str = "\t\t\t\r\r\rMARK_PREVENT_RECURSION\r\r\r\t\t\t"
+
+MARK_CONTINUE: str = "\t\t\t\r\r\rMARK_CONTINUE\r\r\r\t\t\t"
+
+# @see: markdown.util.HTML_PLACEHOLDER_RE
+# PYTHON_MARKDOWN_HTML_PLACEHOLDER_RE: re.Pattern[str] = re.compile(
+# "\u0002wzxhzdk:%s\u0003" % r"([0-9]+)"
+# )
+
+
+class MainExtension(Extension):
+ def extendMarkdown(self, md: Markdown):
+ meta: dict = {
+ "document_offsets": [],
+ "used_document_offsets": {},
+ "last_parent": None,
+ }
+ md.preprocessors.register(
+ CalculateDocumentOffsetPreprocessor(md, meta), "capture_document", 1000
+ ) # Highest priority is required because we need to calc words offset from original document
+ md.preprocessors.register(
+ FixDocumentOffsetPreprocessor(md, meta), "fix_document", 0
+ ) # Lowest priority is required because we need to fix the offset after all other block processors
+ md.parser.blockprocessors.register(
+ OffsetsInjectionBlockProcessor(md.parser, meta), "mark_words", 200
+ ) # high priority, usually larger than every other block processor
+
+
+class CalculateDocumentOffsetPreprocessor(Preprocessor):
+ """
+ A preprocessor to calculate the offset of each line in the document
+ """
+
+ def __init__(self, md: Markdown, meta: dict):
+ super(CalculateDocumentOffsetPreprocessor, self).__init__(md)
+ self.meta = meta
+
+ def run(self, lines: list[str]) -> list[str]:
+ offset: int = 0
+ for line in lines:
+ # Skip empty lines
+ if len(line) == 0:
+ store: tuple[str, int, int] = (line, offset, offset + 1)
+ self.meta["document_offsets"].append(store)
+ self.meta["used_document_offsets"][store] = False
+ offset += 1
+ continue
+ # store the line and offset
+ store: tuple[str, int, int] = (line, offset, offset + len(line))
+ self.meta["document_offsets"].append(store)
+ self.meta["used_document_offsets"][store] = False
+ # plus 1 is for the newline character (\n), use the CRLF file is unknown behavior
+ offset += len(line) + 1
+ return lines
+
+
+class FixDocumentOffsetPreprocessor(Preprocessor):
+ """
+ A preprocessor to fix the offset of each line after the 3rd party extension processed the document
+ """
+
+ def __init__(self, md: Markdown, meta: dict):
+ super(FixDocumentOffsetPreprocessor, self).__init__(md)
+ self.meta = meta
+
+ def run(self, lines: list[str]) -> list[str]:
+ document_offsets: list[tuple[str, int, int]] = self.meta["document_offsets"]
+
+ # 最后一次成功匹配的文档偏移量字典索引末,开区间
+ last_success_match_end: int = 0
+ num_lines: int = 0
+ num_document_offsets: int = 0
+ while num_document_offsets < len(document_offsets) and num_lines < len(lines):
+ line = lines[num_lines]
+ document_offset: tuple[str, int, int] = document_offsets[
+ num_document_offsets
+ ]
+
+ # 如果精准匹配
+ if document_offset[0] == line:
+ # 匹配该行
+ self.match(line, num_document_offsets, num_document_offsets + 1)
+ # 如果上次成功匹配的原文档偏移量未连续,匹配当前行到这部分未连续的原文档偏移量
+ if num_document_offsets > last_success_match_end and num_lines > 0:
+ self.match(
+ lines[num_lines - 1],
+ last_success_match_end,
+ num_document_offsets,
+ )
+ last_success_match_end = num_document_offsets + 1
+ num_lines += 1
+ num_document_offsets += 1
+ # 如果未能精准匹配,查找该行在原文档偏移量字典中的位置
+ else:
+ remain: list[str] = [
+ line for line, _, _ in document_offsets[num_document_offsets:]
+ ]
+ # 如果存在这样的行
+ if line in remain:
+ # 找到第一次匹配的位置,匹配该行到此处
+ idx = remain.index(line) + num_document_offsets
+ self.match(line, idx, idx + 1)
+ # 如果上次成功匹配的原文档偏移量未连续,匹配当前行到这部分未连续的原文档偏移量
+ if idx > last_success_match_end and num_lines > 0:
+ self.match(lines[num_lines - 1], last_success_match_end, idx)
+ last_success_match_end = idx + 1
+ num_lines += 1
+ num_document_offsets = idx + 1
+ # 如果未找到匹配的位置,继续查找下一行
+ else:
+ num_lines += 1
+
+ # 如果行匹配完成,但原文档偏移量未匹配完成,匹配剩余的原文档偏移量
+ if last_success_match_end < len(document_offsets):
+ self.match(
+ lines[num_lines - 1], last_success_match_end, len(document_offsets)
+ )
+
+ return lines
+
+ def match(
+ self,
+ matched_line: str,
+ num_document_offsets_start: int,
+ num_document_offsets_end: int,
+ ):
+ """
+ 将单个匹配行设置到多个原文档偏移量字典,索引范围为[num_document_offsets_start, num_document_offsets_end)
+ """
+ document_offsets: list[tuple[str, int, int]] = self.meta["document_offsets"]
+ used_document_offsets: dict[tuple[str, int, int], bool] = self.meta[
+ "used_document_offsets"
+ ]
+ for i in range(num_document_offsets_start, num_document_offsets_end):
+ document_offset = document_offsets[i]
+ # 如果是第一个匹配的原文档偏移量,设置为匹配行,否则设置为 MARK_CONTINUE
+ if i == num_document_offsets_start:
+ document_offsets[i] = (
+ matched_line,
+ document_offset[1],
+ document_offset[2],
+ )
+ else:
+ document_offsets[i] = (
+ MARK_CONTINUE,
+ document_offset[1],
+ document_offset[2],
+ )
+ del used_document_offsets[document_offset]
+ used_document_offsets[document_offsets[i]] = False
+
+
+class OffsetsInjectionBlockProcessor(BlockProcessor):
+ """
+ A block processor to mark the words in the document and inject the offset of the block to the HTML element
+ """
+
+ def __init__(self, parser: BlockParser, meta: dict):
+ super(OffsetsInjectionBlockProcessor, self).__init__(parser)
+ self.meta = meta
+
+ def test(self, _, block) -> bool:
+ # Test if there is any line in the block
+ for line in [line for (line, _, _) in self.meta["document_offsets"]]:
+ if line in block:
+ return True
+ return False
+
+ def run(self, parent: etree.Element, blocks: list[str]) -> bool:
+ """
+ 注入文档中的偏移量到HTML元素中,以便在后续的处理中可以使用这些偏移量来定位文档中的位置。目前的算法如下:
+ 1. 从文档中查找第一个包含文本的块
+ 2. 查找这个块在文档中的位置,这通过遍历文档中的每一行,以找到所有被包含在该块中的行,通过获取这些行的起始和结束位置,来确定这个块在文档中的位置
+ 3. 注入这个块的起始和结束位置到HTML元素中,这会先递归的解析这个块,然后再注入这个块的起始和结束位置注入到最后一个被生成的HTML元素中
+ 由于递归解析块时该块仍会被本处理器捕获,为了避免循环递归,我们在块的末尾添加了MARK_PREVENT_RECURSION标记,当本处理器再次捕获到这个块时,会直接跳过这个块,并清除这个标记。
+ """
+
+ block: str = blocks[0]
+
+ # If the first block is handled, remove the marker and return, so that other block processors can process it
+ if MARK_PREVENT_RECURSION in blocks[0]:
+ blocks[0] = blocks[0].replace(MARK_PREVENT_RECURSION, "")
+ return False
+
+ start: int | None = None
+ end: int | None = None
+ used: dict[tuple[str, int, int], bool] = {}
+ # Search for the block fragment in the document_offsets
+ for store in self.meta["document_offsets"]:
+ # Skip empty lines
+ if len(store[0]) == 0:
+ continue
+ # If already used, skip
+ if self.meta["used_document_offsets"][store]:
+ continue
+ (line, offset, end_offset) = store
+ # 如果收到 MARK_CONTINUE 标记,直接认为该标记之前的行是连续的
+ if line == MARK_CONTINUE:
+ end = end_offset
+ used[store] = True
+ continue
+ # If found one
+ if line in block:
+ # If the line already scanned (usually some lines with same content in different place), skip
+ if line in [line for (line, _, _) in used.keys()]:
+ continue
+ # If none yet set, set the start offset
+ if start is None:
+ start = offset
+ end = end_offset
+ # Or, continuing searching for the end offset until the end of the block
+ else:
+ end = end_offset
+ # Mark the fragment as used
+ used[store] = True
+ # If end is not found but new line not in block, reset the search and restart from the next line
+ elif end is None:
+ start = None
+ # Clear the used list
+ used = {}
+ continue
+ # If both start and end are both set and no continuously block found, break the loop
+ else:
+ break
+ # If both start and end are found, store the result
+ if start is not None and end is not None:
+ blocks.pop(0)
+ self.meta["used_document_offsets"].update(used)
+ # append MARK_PREVENT_RECURSION to tail of the block to prevent recursion, we don't use a handled
+ # flaglist because we don't know if there's some same block in the document
+ self.parser.parseBlocks(parent, [block + MARK_PREVENT_RECURSION])
+ # fix multi blocks in same parents
+ if self.meta["last_parent"] == parent[-1]:
+ parent[-1].set("data-original-document-end", str(end))
+ return True
+ parent[-1].set("data-original-document-start", str(start))
+ parent[-1].set("data-original-document-end", str(end))
+ self.meta["last_parent"] = parent[-1]
+ return True
+ return False
diff --git a/python-markdown-extension/test/__main__.py b/python-markdown-extension/test/__main__.py
new file mode 100644
index 00000000..93f2eb00
--- /dev/null
+++ b/python-markdown-extension/test/__main__.py
@@ -0,0 +1,375 @@
+import textwrap
+import unittest
+import markdown
+from html.parser import HTMLParser
+
+from pymdownx.emoji import to_svg
+from pymdownx.slugs import uslugify
+from pymdownx.arithmatex import fence_mathjax_format
+
+
+class Tester:
+ def __init__(self, case, test_case: unittest.TestCase):
+ self.case = case
+ """
+ @see: https://github.com/OI-wiki/OI-wiki/blob/65983038c40716dd0644778fe7875e91c9043618/mkdocs.yml#L586
+
+ # Extensions
+ markdown_extensions:
+ - admonition
+ - def_list
+ - footnotes
+ - meta
+ - toc:
+ permalink: ""
+ slugify: !!python/name:pymdownx.slugs.uslugify
+ - pymdownx.arithmatex:
+ generic: true
+ - pymdownx.caret
+ - pymdownx.critic
+ - pymdownx.details
+ - pymdownx.emoji:
+ emoji_generator: !!python/name:pymdownx.emoji.to_svg
+ - pymdownx.highlight:
+ linenums: true
+ - pymdownx.inlinehilite
+ - pymdownx.keys
+ - pymdownx.magiclink
+ - pymdownx.mark
+ - pymdownx.snippets:
+ check_paths: true
+ - pymdownx.progressbar
+ - pymdownx.smartsymbols
+ - pymdownx.superfences:
+ custom_fences:
+ - name: math
+ class: arithmatex
+ format: !!python/name:pymdownx.arithmatex.fence_mathjax_format
+ - pymdownx.tasklist:
+ custom_checkbox: true
+ - pymdownx.tilde
+ - pymdownx.tabbed:
+ alternate_style: true
+ """
+ self.result = markdown.markdown(
+ self.case["document"],
+ extensions=[
+ "document-offsets-injection",
+ "admonition",
+ "def_list",
+ "footnotes",
+ "meta",
+ "toc",
+ "pymdownx.arithmatex",
+ "pymdownx.caret",
+ "pymdownx.critic",
+ "pymdownx.details",
+ "pymdownx.emoji",
+ "pymdownx.highlight",
+ "pymdownx.inlinehilite",
+ "pymdownx.keys",
+ "pymdownx.magiclink",
+ "pymdownx.mark",
+ "pymdownx.snippets",
+ "pymdownx.progressbar",
+ "pymdownx.smartsymbols",
+ "pymdownx.superfences",
+ "pymdownx.tasklist",
+ "pymdownx.tilde",
+ "pymdownx.tabbed",
+ ],
+ extension_configs={
+ "toc": {
+ "permalink": "",
+ "slugify": uslugify,
+ },
+ "pymdownx.arithmatex": {
+ "generic": True,
+ },
+ "pymdownx.emoji": {
+ "emoji_generator": to_svg,
+ },
+ "pymdownx.highlight": {
+ "linenums": True,
+ },
+ "pymdownx.snippets": {
+ "check_paths": True,
+ },
+ "pymdownx.superfences": {
+ "custom_fences": [
+ {
+ "name": "math",
+ "class": "arithmatex",
+ "format": fence_mathjax_format,
+ },
+ ],
+ },
+ "pymdownx.tasklist": {
+ "custom_checkbox": True,
+ },
+ "pymdownx.tabbed": {
+ "alternate_style": True,
+ },
+ },
+ )
+ self.test_case = test_case
+
+ def test(self):
+ tester = ParserTester(self.case, self.test_case)
+ tester.feed(self.result)
+ tester.check_integrity()
+
+
+class ParserTester(HTMLParser):
+ tag = None
+ offset_start = None
+ offset_end = None
+
+ def __init__(self, case, test_case: unittest.TestCase):
+ super().__init__()
+ self.test_case = test_case
+ self.case = case
+ self.idx = 0
+
+ def handle_starttag(self, tag, attrs):
+ start = None
+ end = None
+ for attr in attrs:
+ if attr[0] == "data-original-document-start":
+ start = int(attr[1])
+ if attr[0] == "data-original-document-end":
+ end = int(attr[1])
+ if start is not None and end is not None:
+ self.tag = tag
+ self.offset_start = start
+ self.offset_end = end
+
+ def handle_endtag(self, tag):
+ if self.tag != tag:
+ return # ignore nested tags
+ if self.idx == len(self.case["expected"]):
+ return # ignore extra tags
+ self._test()
+ self._reset()
+
+ def _test(self):
+ self.test_case.assertEqual(
+ self.tag,
+ self.case["expected"][self.idx]["tag"],
+ msg="Tag mismatch in index " + str(self.idx),
+ )
+ self.test_case.assertEqual(
+ self.offset_start,
+ self.case["expected"][self.idx]["offset"][0],
+ msg="Offset start mismatch in index " + str(self.idx),
+ )
+ self.test_case.assertEqual(
+ self.offset_end,
+ self.case["expected"][self.idx]["offset"][1],
+ msg="Offset end mismatch in index " + str(self.idx),
+ )
+ self.idx += 1
+
+ def _reset(self):
+ self.tag = None
+ self.offset_start = None
+ self.offset_end = None
+
+ def check_integrity(self):
+ self.test_case.assertEqual(
+ self.idx,
+ len(self.case["expected"]),
+ msg="Not all tags were found",
+ )
+
+
+class TestParser(unittest.TestCase):
+ def test_normal(self):
+ case = {
+ "document": textwrap.dedent("""\
+ # Lorem ipsum
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin sed lacus vitae neque vestibulum porttitor id et urna.
+
+ ## Morbi neque lectus
+
+ Morbi neque lectus, faucibus a mattis at, aliquam quis est. Maecenas sed luctus elit."""),
+ "expected": [
+ {"tag": "h1", "offset": (0, 13)},
+ {
+ "tag": "p",
+ "offset": (15, 132),
+ },
+ {"tag": "h2", "offset": (134, 155)},
+ {
+ "tag": "p",
+ "offset": (157, 242),
+ },
+ ],
+ }
+ Tester(case, self).test()
+
+ def test_empty(self):
+ case = {
+ "document": "",
+ "expected": [],
+ }
+ Tester(case, self).test()
+
+ def test_single(self):
+ case = {
+ "document": "Lorem ipsum",
+ "expected": [
+ {"tag": "p", "offset": (0, 11)},
+ ],
+ }
+ Tester(case, self).test()
+
+ def test_oi_wiki_index(self):
+ case = {
+ "document": textwrap.dedent("""\
+ disqus:
+ pagetime:
+ title: OI Wiki
+
+ ## 欢迎来到 **OI Wiki**![![GitHub watchers](https://img.shields.io/github/watchers/OI-wiki/OI-wiki.svg?style=social&label=Watch)](https://github.com/OI-wiki/OI-wiki) [![GitHub stars](https://img.shields.io/github/stars/OI-wiki/OI-wiki.svg?style=social&label=Stars)](https://github.com/OI-wiki/OI-wiki)
+
+ [![Word Art](images/wordArt.webp)](https://github.com/OI-wiki/OI-wiki)
+
+ **OI**(Olympiad in Informatics,信息学奥林匹克竞赛)在中国起源于 1984 年,是五大高中学科竞赛之一。
+
+ **ICPC**(International Collegiate Programming Contest,国际大学生程序设计竞赛)由 ICPC 基金会(ICPC Foundation)举办,是最具影响力的大学生计算机竞赛。由于以前 ACM 赞助这个竞赛,也有很多人习惯叫它 ACM 竞赛。
+
+ **OI Wiki** 致力于成为一个免费开放且持续更新的 **编程竞赛(competitive programming)** 知识整合站点,大家可以在这里获取与竞赛相关的、有趣又实用的知识。我们为大家准备了竞赛中的基础知识、常见题型、解题思路以及常用工具等内容,帮助大家更快速深入地学习编程竞赛中涉及到的知识。
+
+ 本项目受 [CTF Wiki](https://ctf-wiki.org/) 的启发,在编写过程中参考了诸多资料,在此一并致谢。
+
+
+
+
+
+ """),
+ "expected": [
+ {
+ "tag": "h2",
+ "offset": (34, 332),
+ },
+ {
+ "tag": "p",
+ "offset": (334, 404),
+ },
+ {
+ "tag": "p",
+ "offset": (406, 473),
+ },
+ {
+ "tag": "p",
+ "offset": (475, 620),
+ },
+ {
+ "tag": "p",
+ "offset": (622, 778),
+ },
+ {
+ "tag": "p",
+ "offset": (780, 1101), # FIXME: Correct one is (780, 1101)
+ },
+ ],
+ }
+ Tester(case, self).test()
+
+ def test_oi_wiki_search_dfs(self):
+ case = {
+ "document": textwrap.dedent("""\
+ ## 引入
+
+ DFS 为图论中的概念,详见 [DFS(图论)](../graph/dfs.md) 页面。在 **搜索算法** 中,该词常常指利用递归函数方便地实现暴力枚举的算法,与图论中的 DFS 算法有一定相似之处,但并不完全相同。
+
+ ## 解释
+
+ 考虑这个例子:
+
+ ???+ note "例题"
+ 把正整数 $n$ 分解为 $3$ 个不同的正整数,如 $6=1+2+3$,排在后面的数必须大于等于前面的数,输出所有方案。
+
+ 对于这个问题,如果不知道搜索,应该怎么办呢?
+
+ 当然是三重循环,参考代码如下:
+
+ ???+ note "实现"
+ === "C++"
+ ```cpp
+ for (int i = 1; i <= n; ++i)
+ for (int j = i; j <= n; ++j)
+ for (int k = j; k <= n; ++k)
+ if (i + j + k == n) printf("%d = %d + %d + %d\\n", n, i, j, k);
+ ```
+
+ === "Python"
+ ```python
+ for i in range(1, n + 1):
+ for j in range(i, n + 1):
+ for k in range(j, n + 1):
+ if i + j + k == n:
+ print("%d = %d + %d + %d" % (n, i, j, k))
+ ```
+
+ === "Java"
+ ```Java
+ for (int i = 1; i < n + 1; i++) {
+ for (int j = i; j < n + 1; j++) {
+ for (int k = j; k < n + 1; k++) {
+ if (i + j + k == n) System.out.printf("%d = %d + %d + %d%n", n, i, j, k);
+ }
+ }
+ }
+ ```
+
+ 那如果是分解成四个整数呢?再加一重循环?"""),
+ "expected": [
+ {
+ "tag": "h2",
+ "offset": (0, 5),
+ },
+ {
+ "tag": "p",
+ "offset": (7, 117),
+ },
+ {
+ "tag": "h2",
+ "offset": (119, 124),
+ },
+ {
+ "tag": "p",
+ "offset": (126, 133),
+ },
+ {
+ "tag": "details",
+ "offset": (135, 215),
+ },
+ {
+ "tag": "p",
+ "offset": (217, 239),
+ },
+ {
+ "tag": "p",
+ "offset": (241, 256),
+ },
+ {
+ "tag": "details",
+ "offset": (258, 1092),
+ },
+ {
+ "tag": "p",
+ "offset": (1094, 1114),
+ },
+ ],
+ }
+ Tester(case, self).test()
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/src/counter.ts b/src/counter.ts
deleted file mode 100644
index 09e5afd2..00000000
--- a/src/counter.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export function setupCounter(element: HTMLButtonElement) {
- let counter = 0
- const setCounter = (count: number) => {
- counter = count
- element.innerHTML = `count is ${counter}`
- }
- element.addEventListener('click', () => setCounter(counter + 1))
- setCounter(0)
-}
diff --git a/src/main.ts b/src/main.ts
deleted file mode 100644
index 791547b0..00000000
--- a/src/main.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import './style.css'
-import typescriptLogo from './typescript.svg'
-import viteLogo from '/vite.svg'
-import { setupCounter } from './counter.ts'
-
-document.querySelector('#app')!.innerHTML = `
-
-
-
-
-
-
-
-
Vite + TypeScript
-
-
-
-
- Click on the Vite and TypeScript logos to learn more
-
-
-`
-
-setupCounter(document.querySelector('#counter')!)
diff --git a/src/style.css b/src/style.css
deleted file mode 100644
index f9c73502..00000000
--- a/src/style.css
+++ /dev/null
@@ -1,96 +0,0 @@
-:root {
- font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
- line-height: 1.5;
- font-weight: 400;
-
- color-scheme: light dark;
- color: rgba(255, 255, 255, 0.87);
- background-color: #242424;
-
- font-synthesis: none;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-a {
- font-weight: 500;
- color: #646cff;
- text-decoration: inherit;
-}
-a:hover {
- color: #535bf2;
-}
-
-body {
- margin: 0;
- display: flex;
- place-items: center;
- min-width: 320px;
- min-height: 100vh;
-}
-
-h1 {
- font-size: 3.2em;
- line-height: 1.1;
-}
-
-#app {
- max-width: 1280px;
- margin: 0 auto;
- padding: 2rem;
- text-align: center;
-}
-
-.logo {
- height: 6em;
- padding: 1.5em;
- will-change: filter;
- transition: filter 300ms;
-}
-.logo:hover {
- filter: drop-shadow(0 0 2em #646cffaa);
-}
-.logo.vanilla:hover {
- filter: drop-shadow(0 0 2em #3178c6aa);
-}
-
-.card {
- padding: 2em;
-}
-
-.read-the-docs {
- color: #888;
-}
-
-button {
- border-radius: 8px;
- border: 1px solid transparent;
- padding: 0.6em 1.2em;
- font-size: 1em;
- font-weight: 500;
- font-family: inherit;
- background-color: #1a1a1a;
- cursor: pointer;
- transition: border-color 0.25s;
-}
-button:hover {
- border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
- outline: 4px auto -webkit-focus-ring-color;
-}
-
-@media (prefers-color-scheme: light) {
- :root {
- color: #213547;
- background-color: #ffffff;
- }
- a:hover {
- color: #747bff;
- }
- button {
- background-color: #f9f9f9;
- }
-}
diff --git a/src/typescript.svg b/src/typescript.svg
deleted file mode 100644
index d91c910c..00000000
--- a/src/typescript.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
deleted file mode 100644
index 11f02fe2..00000000
--- a/src/vite-env.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-///
diff --git a/tsconfig.json b/tsconfig.json
deleted file mode 100644
index 75abdef2..00000000
--- a/tsconfig.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2020",
- "useDefineForClassFields": true,
- "module": "ESNext",
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
- "skipLibCheck": true,
-
- /* Bundler mode */
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "resolveJsonModule": true,
- "isolatedModules": true,
- "noEmit": true,
-
- /* Linting */
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noFallthroughCasesInSwitch": true
- },
- "include": ["src"]
-}