diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 2c98cd5..5d05853 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -3,7 +3,12 @@ "allow": [ "Bash(python:*)", "Bash(tree:*)", - "Bash(dir:*)" + "Bash(dir:*)", + "Bash(python3 -m venv venv)", + "Bash(source venv/bin/activate)", + "Bash(pip install:*)", + "Bash(source /mnt/d/code/sync-pd2-wiki/venv/bin/activate)", + "Bash(curl -s \"https://wiki.projectdiablo2.cn/w/api.php?action=query&prop=revisions&titles=Filter%20Info&rvprop=ids&format=json\")" ] } } diff --git a/.claude/skills/mediawiki-wikitext/SKILL.md b/.claude/skills/mediawiki-wikitext/SKILL.md new file mode 100644 index 0000000..89b1ab7 --- /dev/null +++ b/.claude/skills/mediawiki-wikitext/SKILL.md @@ -0,0 +1,148 @@ +--- +name: mediawiki-wikitext +description: MediaWiki Wikitext markup language for Wikipedia and wiki-based sites. Use when creating or editing wiki articles, generating wikitext content, working with wiki tables/templates/references, or converting content to wikitext format. Triggers on requests mentioning Wikipedia, MediaWiki, wikitext, wiki markup, or wiki article creation. +--- + +# MediaWiki Wikitext + +Generate and edit content using MediaWiki's wikitext markup language. + +## Quick Reference + +### Text Formatting +```wikitext +''italic'' '''bold''' '''''bold italic''''' +inline code subscript superscript +strikethrough underline +``` + +### Headings (line start only, avoid level 1) +```wikitext +== Level 2 == +=== Level 3 === +==== Level 4 ==== +``` + +### Lists +```wikitext +* Bullet item # Numbered item ; Term +** Nested ## Nested : Definition +``` + +### Links +```wikitext +[[Page Name]] Internal link +[[Page Name|Display Text]] With display text +[[Page Name#Section]] Section link +[https://url Display Text] External link +[[File:image.jpg|thumb|Caption]] Image +[[Category:Name]] Category (place at end) +``` + +### Table +```wikitext +{| class="wikitable" +|+ Caption +|- +! Header 1 !! Header 2 +|- +| Cell 1 || Cell 2 +|} +``` + +### Templates & Variables +```wikitext +{{TemplateName}} Basic call +{{TemplateName|arg1|name=value}} With arguments +{{{parameter|default}}} Parameter (in template) +{{PAGENAME}} {{CURRENTYEAR}} Magic words +``` + +### References +```wikitext +TextCitation here +Citation Named reference + Reuse reference +{{Reflist}} Display footnotes +``` + +### Special Tags +```wikitext +[[escaped]] Disable markup +
preformatted block
Preformatted (no markup) + Code highlighting +code here + +x^2 + y^2 = z^2 LaTeX math + Comment (hidden) +---- Horizontal rule +#REDIRECT [[Target Page]] Redirect (first line only) +``` + +### Magic Words +```wikitext +__NOTOC__ Hide table of contents +__TOC__ Position TOC here +__NOEDITSECTION__ Hide section edit links +``` + +## Common Patterns + +### Article Structure +```wikitext +{{Infobox Type +| name = Example +| image = Example.jpg +}} + +'''Article Title''' is a brief introduction. + +== Section == +Content with citationSource. + +=== Subsection === +More content. + +== See also == +* [[Related Article]] + +== References == +{{Reflist}} + +== External links == +* [https://example.com Official site] + +{{DEFAULTSORT:Sort Key}} +[[Category:Category Name]] +``` + +### Template Definition +```wikitext +{{Documentation}} +{| class="wikitable" +! {{{title|Default Title}}} +|- +| {{{content|No content provided}}} +{{#if:{{{footer|}}}| +{{!}}- +{{!}} {{{footer}}} +}} +|} + +``` + +## Key Syntax Rules + +1. **Headings**: Use `==` to `======`; don't use `=` (reserved for page title) +2. **Line-start markup**: Lists (`*#;:`), headings, tables (`{|`) must start at line beginning +3. **Closing tags**: Close heading equals on same line; no text after closing `==` +4. **Blank lines**: Create paragraph breaks; single newlines are ignored +5. **Pipes in templates**: Use `{{!}}` for literal `|` inside templates +6. **Escaping**: Use `` to escape markup; `&` for `&`, `<` for `<` + +## Resources + +For detailed syntax, see: +- **references/syntax.md**: Complete markup reference with all options +- **references/templates.md**: Template and parser function details +- **assets/snippets.yaml**: Editor snippets for common patterns diff --git a/.claude/skills/mediawiki-wikitext/assets/snippets.yaml b/.claude/skills/mediawiki-wikitext/assets/snippets.yaml new file mode 100644 index 0000000..ffe11c9 --- /dev/null +++ b/.claude/skills/mediawiki-wikitext/assets/snippets.yaml @@ -0,0 +1,236 @@ +# MediaWiki Wikitext Snippets +# For VS Code and compatible editors + +--- +# Headings +h2: + prefix: '@h2' + body: "== ${1:Heading} ==\n" + description: Level 2 heading + +h3: + prefix: '@h3' + body: "=== ${1:Heading} ===\n" + description: Level 3 heading + +h4: + prefix: '@h4' + body: "==== ${1:Heading} ====\n" + description: Level 4 heading + +# Text formatting +bold: + prefix: '@bold' + body: "'''${1:text}'''" + description: Bold text + +italic: + prefix: '@italic' + body: "''${1:text}''" + description: Italic text + +# Links +link: + prefix: '@link' + body: '[[${1:Page}]]' + description: Internal link + +linkd: + prefix: '@linkd' + body: '[[${1:Page}|${2:Display}]]' + description: Link with display text + +elink: + prefix: '@elink' + body: '[${1:https://} ${2:Text}]' + description: External link + +file: + prefix: '@file' + body: '[[File:${1:name.jpg}|thumb|${2:Caption}]]' + description: Image/file + +cat: + prefix: '@cat' + body: '[[Category:${1:Name}]]' + description: Category + +# Table +table: + prefix: '@table' + body: | + {| class="wikitable" + |+ ${1:Caption} + |- + ! ${2:Header1} !! ${3:Header2} + |- + | ${4:Cell1} || ${5:Cell2} + |} + description: Basic table + +tr: + prefix: '@tr' + body: | + |- + | ${1} || ${2} + description: Table row + +# References +ref: + prefix: '@ref' + body: '${1:Citation}' + description: Reference + +refn: + prefix: '@refn' + body: '${2:Citation}' + description: Named reference + +refr: + prefix: '@refr' + body: '' + description: Reference reuse + +reflist: + prefix: '@reflist' + body: | + == References == + {{Reflist}} + description: References section + +# Templates +tpl: + prefix: '@tpl' + body: '{{${1:Template}}}' + description: Template call + +tplp: + prefix: '@tplp' + body: '{{${1:Template}|${2:param}=${3:value}}}' + description: Template with params + +infobox: + prefix: '@infobox' + body: | + {{Infobox ${1:type} + | name = ${2} + | image = ${3} + }} + description: Infobox + +# Code +code: + prefix: '@code' + body: | + + ${2:code} + + description: Syntax highlight block + +codei: + prefix: '@codei' + body: '${1:code}' + description: Inline code + +nowiki: + prefix: '@nowiki' + body: '${1:text}' + description: Escape markup + +pre: + prefix: '@pre' + body: | +
+    ${1:text}
+    
+ description: Preformatted block + +math: + prefix: '@math' + body: '${1:formula}' + description: Math formula + +# Comments +comment: + prefix: '@comment' + body: '' + description: Comment + +todo: + prefix: '@todo' + body: '' + description: TODO comment + +# Magic words +notoc: + prefix: '@notoc' + body: '__NOTOC__' + description: Hide TOC + +toc: + prefix: '@toc' + body: '__TOC__' + description: TOC position + +# Lists +ul: + prefix: '@ul' + body: | + * ${1:Item 1} + * ${2:Item 2} + * ${3:Item 3} + description: Bullet list + +ol: + prefix: '@ol' + body: | + # ${1:Item 1} + # ${2:Item 2} + # ${3:Item 3} + description: Numbered list + +dl: + prefix: '@dl' + body: | + ; ${1:Term} + : ${2:Definition} + description: Definition list + +# Structure +redirect: + prefix: '@redirect' + body: '#REDIRECT [[${1:Target}]]' + description: Redirect + +article: + prefix: '@article' + body: | + {{Infobox + | name = ${1:Name} + }} + + '''${2:Title}''' is ${3:description}. + + == Overview == + ${4:Content} + + == References == + {{Reflist}} + + [[Category:${5:Category}]] + description: Article template + +hr: + prefix: '@hr' + body: '----' + description: Horizontal rule + +br: + prefix: '@br' + body: '
' + description: Line break + +sort: + prefix: '@sort' + body: '{{DEFAULTSORT:${1:Key}}}' + description: Default sort key diff --git a/.claude/skills/mediawiki-wikitext/references/syntax.md b/.claude/skills/mediawiki-wikitext/references/syntax.md new file mode 100644 index 0000000..fd609df --- /dev/null +++ b/.claude/skills/mediawiki-wikitext/references/syntax.md @@ -0,0 +1,345 @@ +# MediaWiki Wikitext Syntax Reference + +Complete syntax reference for MediaWiki wikitext markup. + +## Text Formatting (Inline) + +| Syntax | Result | Notes | +|--------|--------|-------| +| `''text''` | *italic* | Two single quotes | +| `'''text'''` | **bold** | Three single quotes | +| `'''''text'''''` | ***bold italic*** | Five single quotes | +| `text` | `monospace` | Inline code | +| `x` | Variable style | | +| `Ctrl` | Keyboard input | | +| `output` | Sample output | | +| `2` | Subscript | H₂O | +| `2` | Superscript | x² | +| `text` | ~~strikethrough~~ | | +| `text` | Deleted text | Semantic | +| `text` | Inserted text | Often underlined | +| `text` | Underline | | +| `text` | Small text | | +| `text` | Large text | Deprecated | + +## Headings + +```wikitext += Level 1 = (Don't use - reserved for page title) +== Level 2 == +=== Level 3 === +==== Level 4 ==== +===== Level 5 ===== +====== Level 6 ====== +``` + +**Rules:** +- Must start at line beginning +- No text after closing equals on same line +- 4+ headings auto-generate TOC (unless `__NOTOC__`) +- Spaces around heading text are optional but recommended + +## Lists + +### Unordered (Bullet) Lists +```wikitext +* Item 1 +* Item 2 +** Nested item 2.1 +** Nested item 2.2 +*** Deeper nesting +* Item 3 +``` + +### Ordered (Numbered) Lists +```wikitext +# First item +# Second item +## Sub-item 2.1 +## Sub-item 2.2 +### Sub-sub-item +# Third item +``` + +### Definition Lists +```wikitext +; Term 1 +: Definition 1 +; Term 2 +: Definition 2a +: Definition 2b +``` + +### Mixed Lists +```wikitext +# Numbered item +#* Bullet under numbered +#* Another bullet +# Next numbered +#: Definition-style continuation +``` + +### Indentation +```wikitext +: Single indent +:: Double indent +::: Triple indent +``` + +**Note:** List markers must be at line start. Blank lines end the list. + +## Links + +### Internal Links +```wikitext +[[Page Name]] +[[Page Name|Display Text]] +[[Page Name#Section]] +[[Page Name#Section|Display Text]] +[[Namespace:Page Name]] +[[/Subpage]] +[[../Sibling Page]] +``` + +### External Links +```wikitext +[https://example.com] Numbered link [1] +[https://example.com Display Text] Named link +https://example.com Auto-linked +``` + +### Special Links +```wikitext +[[File:Image.jpg]] Embed image +[[File:Image.jpg|thumb|Caption]] Thumbnail with caption +[[File:Image.jpg|thumb|left|200px|Caption]] +[[Media:File.pdf]] Direct file link +[[Category:Category Name]] Add to category +[[:Category:Category Name]] Link to category (no add) +[[Special:RecentChanges]] Special page +``` + +### Interwiki Links +```wikitext +[[en:English Article]] Language link +[[wikt:word]] Wiktionary +[[commons:File:Image.jpg]] Wikimedia Commons +``` + +## Images + +### Basic Syntax +```wikitext +[[File:Example.jpg|options|caption]] +``` + +### Image Options + +| Option | Description | +|--------|-------------| +| `thumb` | Thumbnail (default right-aligned) | +| `frame` | Framed, no resize | +| `frameless` | Thumbnail without frame | +| `border` | Thin border | +| `right`, `left`, `center`, `none` | Alignment | +| `200px` | Width | +| `x100px` | Height | +| `200x100px` | Max dimensions | +| `upright` | Smart scaling for tall images | +| `upright=0.5` | Custom ratio | +| `link=Page` | Custom link target | +| `link=` | No link | +| `alt=Text` | Alt text for accessibility | + +### Gallery +```wikitext + +File:Image1.jpg|Caption 1 +File:Image2.jpg|Caption 2 + + + +File:Image1.jpg +File:Image2.jpg + +``` + +## Tables + +### Basic Structure +```wikitext +{| class="wikitable" +|+ Caption +|- +! Header 1 !! Header 2 !! Header 3 +|- +| Cell 1 || Cell 2 || Cell 3 +|- +| Cell 4 || Cell 5 || Cell 6 +|} +``` + +### Table Elements + +| Markup | Location | Meaning | +|--------|----------|---------| +| `{|` | Start | Table start | +| `|}` | End | Table end | +| `|+` | After `{|` | Caption | +| `|-` | Row | Row separator | +| `!` | Cell | Header cell | +| `!!` | Cell | Header cell separator (same row) | +| `|` | Cell | Data cell | +| `||` | Cell | Data cell separator (same row) | + +### Cell Attributes +```wikitext +| style="background:#fcc" | Red background +| colspan="2" | Spans 2 columns +| rowspan="3" | Spans 3 rows +! scope="col" | Column header +! scope="row" | Row header +``` + +### Sortable Table +```wikitext +{| class="wikitable sortable" +|- +! Name !! Value +|- +| Alpha || 1 +| Beta || 2 +|} +``` + +## References + +### Basic Citation +```wikitext +StatementSource information + +== References == +{{Reflist}} +``` + +### Named References +```wikitext +First useSmith, 2020, p. 42 +Second use +``` + +### Grouped References +```wikitext +NoteExplanatory note +SourceRegular citation + +== Notes == +{{Reflist|group="note"}} + +== References == +{{Reflist}} +``` + +## Special Tags + +### nowiki (Escape Markup) +```wikitext +[[Not a link]] +<nowiki> Outputs: +``` + +### pre (Preformatted) +```wikitext +
+Preformatted text
+  Whitespace preserved
+  '''Markup not processed'''
+
+``` + +### syntaxhighlight (Code) +```wikitext + +def hello(): + print("Hello") + + + +# Line numbers starting at 10 + +``` + +Supported languages: python, javascript, php, java, c, cpp, csharp, ruby, perl, sql, xml, html, css, json, yaml, bash, etc. + +### math (LaTeX) +```wikitext +Inline: E = mc^2 +Block: \sum_{i=1}^n i = \frac{n(n+1)}{2} +Chemistry: H2O +``` + +### Transclusion Control +```wikitext +Only when transcluded +Only on template page itself +Only this part is transcluded +``` + +## HTML Entities + +| Entity | Character | Description | +|--------|-----------|-------------| +| `&` | & | Ampersand | +| `<` | < | Less than | +| `>` | > | Greater than | +| ` ` | (space) | Non-breaking space | +| `—` | — | Em dash | +| `–` | – | En dash | +| `→` | → | Right arrow | +| `←` | ← | Left arrow | +| `©` | © | Copyright | +| `€` | € | Euro | +| `:` | : | Colon (in definition lists) | + +## Miscellaneous + +### Horizontal Rule +```wikitext +---- +``` + +### Comments +```wikitext + + +``` + +### Line Breaks +```wikitext +Line 1
Line 2 +``` + +### Redirect +```wikitext +#REDIRECT [[Target Page]] +#REDIRECT [[Target Page#Section]] +``` +Must be first line of page. + +### Signatures (Talk pages) +```wikitext +~~~ Username only +~~~~ Username and timestamp +~~~~~ Timestamp only +``` + +### Categories +```wikitext +[[Category:Category Name]] +[[Category:Category Name|Sort Key]] +{{DEFAULTSORT:Sort Key}} +``` +Place at end of article. diff --git a/.claude/skills/mediawiki-wikitext/references/templates.md b/.claude/skills/mediawiki-wikitext/references/templates.md new file mode 100644 index 0000000..98cfc2c --- /dev/null +++ b/.claude/skills/mediawiki-wikitext/references/templates.md @@ -0,0 +1,311 @@ +# MediaWiki Templates and Parser Functions + +## Template Basics + +### Calling Templates +```wikitext +{{TemplateName}} +{{TemplateName|positional arg}} +{{TemplateName|param1=value1|param2=value2}} +{{TemplateName +| param1 = value1 +| param2 = value2 +}} +``` + +### Template Parameters (Definition Side) +```wikitext +{{{1}}} First positional parameter +{{{paramName}}} Named parameter +{{{1|default}}} With default value +{{{paramName|}}} Empty default (vs undefined) +``` + +### Transclusion +```wikitext +{{:Page Name}} Transclude article (with colon) +{{Template Name}} Transclude template +{{subst:Template Name}} Substitute (one-time expansion) +{{safesubst:Template}} Safe substitution +{{msgnw:Template}} Show raw wikitext +``` + +## Parser Functions + +### Conditionals + +#### #if (empty test) +```wikitext +{{#if: {{{param|}}} | not empty | empty or undefined }} +{{#if: {{{param|}}} | has value }} +``` + +#### #ifeq (equality test) +```wikitext +{{#ifeq: {{{type}}} | book | It's a book | Not a book }} +{{#ifeq: {{{1}}} | {{{2}}} | same | different }} +``` + +#### #iferror +```wikitext +{{#iferror: {{#expr: 1/0}} | Division error | OK }} +``` + +#### #ifexist (page exists) +```wikitext +{{#ifexist: Page Name | [[Page Name]] | Page doesn't exist }} +``` + +#### #ifexpr (expression test) +```wikitext +{{#ifexpr: {{{count}}} > 10 | Many | Few }} +{{#ifexpr: {{{year}}} mod 4 = 0 | Leap year candidate }} +``` + +#### #switch +```wikitext +{{#switch: {{{type}}} +| book = 📚 Book +| article = 📄 Article +| website = 🌐 Website +| #default = 📋 Other +}} + +{{#switch: {{{1}}} +| A | B | C = First three letters +| #default = Something else +}} +``` + +### String Functions + +#### #len +```wikitext +{{#len: Hello }} Returns: 5 +``` + +#### #pos (find position) +```wikitext +{{#pos: Hello World | o }} Returns: 4 (first 'o') +{{#pos: Hello World | o | 5 }} Returns: 7 (after position 5) +``` + +#### #sub (substring) +```wikitext +{{#sub: Hello World | 0 | 5 }} Returns: Hello +{{#sub: Hello World | 6 }} Returns: World +{{#sub: Hello World | -5 }} Returns: World (from end) +``` + +#### #replace +```wikitext +{{#replace: Hello World | World | Universe }} Returns: Hello Universe +``` + +#### #explode (split) +```wikitext +{{#explode: a,b,c,d | , | 2 }} Returns: c (third element) +``` + +#### #urlencode / #urldecode +```wikitext +{{#urlencode: Hello World }} Returns: Hello%20World +{{#urldecode: Hello%20World }} Returns: Hello World +``` + +### Math Functions + +#### #expr +```wikitext +{{#expr: 1 + 2 * 3 }} Returns: 7 +{{#expr: (1 + 2) * 3 }} Returns: 9 +{{#expr: 2 ^ 10 }} Returns: 1024 +{{#expr: 17 mod 5 }} Returns: 2 +{{#expr: floor(3.7) }} Returns: 3 +{{#expr: ceil(3.2) }} Returns: 4 +{{#expr: round(3.567, 2) }} Returns: 3.57 +{{#expr: abs(-5) }} Returns: 5 +{{#expr: sqrt(16) }} Returns: 4 +{{#expr: ln(e) }} Returns: 1 +{{#expr: sin(pi/2) }} Returns: 1 +``` + +**Operators:** `+`, `-`, `*`, `/`, `^` (power), `mod`, `round`, `floor`, `ceil`, `abs`, `sqrt`, `ln`, `exp`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `pi`, `e` + +**Comparison:** `=`, `<>`, `!=`, `<`, `>`, `<=`, `>=` + +**Logical:** `and`, `or`, `not` + +### Date/Time Functions + +#### #time +```wikitext +{{#time: Y-m-d }} Current: 2024-01-15 +{{#time: F j, Y | 2024-01-15 }} January 15, 2024 +{{#time: Y年n月j日 | 2024-01-15 }} 2024年1月15日 +{{#time: l | 2024-01-15 }} Monday +``` + +**Format codes:** +| Code | Output | Description | +|------|--------|-------------| +| Y | 2024 | 4-digit year | +| y | 24 | 2-digit year | +| n | 1 | Month (no leading zero) | +| m | 01 | Month (with leading zero) | +| F | January | Full month name | +| M | Jan | Abbreviated month | +| j | 5 | Day (no leading zero) | +| d | 05 | Day (with leading zero) | +| l | Monday | Full weekday | +| D | Mon | Abbreviated weekday | +| H | 14 | Hour (24h, leading zero) | +| i | 05 | Minutes (leading zero) | +| s | 09 | Seconds (leading zero) | + +#### #timel (local time) +```wikitext +{{#timel: H:i }} Local time +``` + +### Formatting Functions + +#### #formatnum +```wikitext +{{#formatnum: 1234567.89 }} 1,234,567.89 +{{#formatnum: 1,234.56 | R }} 1234.56 (raw) +``` + +#### #padleft / #padright +```wikitext +{{#padleft: 7 | 3 | 0 }} 007 +{{#padright: abc | 6 | . }} abc... +``` + +#### #lc / #uc / #lcfirst / #ucfirst +```wikitext +{{#lc: HELLO }} hello +{{#uc: hello }} HELLO +{{#lcfirst: HELLO }} hELLO +{{#ucfirst: hello }} Hello +{{lc: HELLO }} hello (shortcut) +``` + +### Other Functions + +#### #tag +```wikitext +{{#tag: ref | Citation text | name=smith }} +Equivalent to: Citation text +``` + +#### #invoke (Lua modules) +```wikitext +{{#invoke: ModuleName | functionName | arg1 | arg2 }} +``` + +## Magic Words + +### Behavior Switches +```wikitext +__NOTOC__ No table of contents +__FORCETOC__ Force TOC even with <4 headings +__TOC__ Place TOC here +__NOEDITSECTION__ No section edit links +__NEWSECTIONLINK__ Add new section link +__NONEWSECTIONLINK__ Remove new section link +__NOGALLERY__ No gallery in category +__HIDDENCAT__ Hidden category +__INDEX__ Index by search engines +__NOINDEX__ Don't index +__STATICREDIRECT__ Don't update redirect +``` + +### Page Variables +```wikitext +{{PAGENAME}} Page title without namespace +{{FULLPAGENAME}} Full page title +{{BASEPAGENAME}} Parent page name +{{SUBPAGENAME}} Subpage name +{{ROOTPAGENAME}} Root page name +{{TALKPAGENAME}} Associated talk page +{{NAMESPACE}} Current namespace +{{NAMESPACENUMBER}} Namespace number +{{PAGEID}} Page ID +{{REVISIONID}} Revision ID +``` + +### Site Variables +```wikitext +{{SITENAME}} Wiki name +{{SERVER}} Server URL +{{SERVERNAME}} Server hostname +{{SCRIPTPATH}} Script path +``` + +### Date/Time Variables +```wikitext +{{CURRENTYEAR}} 4-digit year +{{CURRENTMONTH}} Month (01-12) +{{CURRENTMONTHNAME}} Month name +{{CURRENTDAY}} Day (1-31) +{{CURRENTDAYNAME}} Day name +{{CURRENTTIME}} HH:MM +{{CURRENTTIMESTAMP}} YYYYMMDDHHmmss +``` + +### Statistics +```wikitext +{{NUMBEROFPAGES}} Total pages +{{NUMBEROFARTICLES}} Content pages +{{NUMBEROFFILES}} Files +{{NUMBEROFUSERS}} Registered users +{{NUMBEROFACTIVEUSERS}} Active users +{{NUMBEROFEDITS}} Total edits +{{PAGESINCATEGORY:Name}} Pages in category +``` + +## Template Examples + +### Simple Infobox +```wikitext +{{Documentation}} +{| class="infobox" style="width:22em" +|- +! colspan="2" style="background:#ccc" | {{{title|{{PAGENAME}}}}} +{{#if:{{{image|}}}| +{{!}}- +{{!}} colspan="2" {{!}} [[File:{{{image}}}|200px|center]] +}} +|- +| '''Type''' || {{{type|Unknown}}} +|- +| '''Date''' || {{{date|—}}} +|} + +``` + +### Navbox Template +```wikitext +{{Documentation}} +{| class="navbox" style="width:100%" +|- +! style="background:#ccf" | {{{title|Navigation}}} +|- +| {{{content|}}} +|} + +``` + +### Citation Template +```wikitext +{{#if:{{{author|}}}|{{{author}}}. }}{{#if:{{{title|}}}|''{{{title}}}''. }}{{#if:{{{publisher|}}}|{{{publisher}}}{{#if:{{{year|}}}|, }}}}{{{year|}}}.{{#if:{{{url|}}}| [{{{url}}} Link]}} +``` + +## Tips + +1. **Pipe trick**: `[[Help:Contents|]]` displays as "Contents" +2. **Escape pipes in templates**: Use `{{!}}` for literal `|` +3. **Trim whitespace**: Parameters automatically trim whitespace +4. **Check emptiness correctly**: `{{{param|}}}` vs `{{{param}}}` - the former has empty default, latter is undefined if not passed +5. **Subst for speed**: Use `{{subst:Template}}` for templates that don't need dynamic updates diff --git a/.claude/skills/wiki-sync-translate/SKILL.md b/.claude/skills/wiki-sync-translate/SKILL.md new file mode 100644 index 0000000..2bc5a5e --- /dev/null +++ b/.claude/skills/wiki-sync-translate/SKILL.md @@ -0,0 +1,139 @@ +--- +name: wiki-sync-translate +description: 同步英文 MediaWiki 页面变更到中文翻译文档。当用户需要更新中文 Wiki页面、同步英文变更、或翻译 Wiki 内容时触发。适用于 Project Diablo 2 Wiki 的中英双语同步维护场景。 +--- + +# Wiki 同步翻译 + +同步英文 Wiki 页面变更到中文翻译文档,保持行号一致。 + +## 使用方法 + +- 用户请求更新中文 Wiki 页面 +- 用户请求同步英文 Wiki 变更 +- 用户指定某个页面需要进行翻译更新 +- 用户执行 `/wiki-sync-translate <页面名称>` + +## 工作目录 + +脚本位于:`.claude/skills/wiki-sync-translate/scripts/wiki_sync.py` + +## 执行步骤 + +### Step 1: 运行同步脚本 + +使用 skill 目录下的专用脚本获取变更: + +```bash +cd /mnt/d/code/sync-pd2-wiki +source venv/bin/activate +python .claude/skills/wiki-sync-translate/ +scripts/wiki_sync.py --title "<页面名称>" --since <上次同步时间> --run +``` + +参数说明: +- `--title`: 指定要同步的页面名称 +- `--since`: 起始时间,格式如 `2026-01-02T12:07:05Z` +- `--run`: 必须提供此参数才会执行 + +### Step 2: 读取输出文件 + +脚本会在 `wiki_sync_output/<时间戳>/changed_pages/` 目录下生成: + +| 文件 | 说明 | 用途 | +|------|------|------| +| `*.comparison.json` | 结构化变更信息 | **AI 读取,包含行号和变更内容** | +| `*.full.txt` | 英文最新版本 | 参考对照 | +| `*.cn.txt` | 中文原文 | **基于此文件进行修改** | +| `*.old.txt` | 英文历史版本 | 参考对照 | + + +### Step 3: 解析 comparison.json +`comparison.json` 格式: + +```json +{ + "title": "页面标题", + "has_cn_translation": true, + "summary": { + "total_changes": 1, + "replaced": 1, + "added": 0, + "removed": 0 + }, + "changes": [ + { + "type": "replaced", + "old_line": 66, + "new_line": 66, + "old_content": "旧内容", + "new_content": "新内容" + } + ] +} +``` + +变更类型: +- `replaced`: 替换,`old_line` 表示需要修改的行号 +- `added`: 新增,`new_line` 表示插入位置 +- `removed`: 删除,`old_line` 表示要删除的行 + +### Step 4: 更新中文文档 + +**核心原则:行号必须完全一致** + +1. 创建 `wiki_sync_output/<时间戳>/result_pages/` 目录(如不存在) +2. 复制 `*.cn.txt` 内容到 `result_pages/<页面名>.cn.txt` +3. 根据 `changes` 中的行号定位对应行 +4. 智能更新: + - 仅同步变更的内容 + - 保留中文翻译(如 "赛季 12" 不改为 "Season 12") + - 新增的英文内容智能翻译成中文,可以用`grep` 命令在`references/PatchString.txt` 搜索技能或物品的中文名称,搜索不到的则智能翻译按照暗黑破坏神2的常用命名。 + - 保持 MediaWiki 语法正确,语法可以参照`mediawiki-wikitest` skill. +5. 保存到 `wiki_sync_output/<时间戳>/result_pages/<页面名>.cn.txt` + +### Step 5: 输出结果 + +将更新后的文档保存到 `wiki_sync_output/<时间戳>/result_pages/<页面名>.cn.txt`,用户可直接复制到 Wiki。 + + ## 示例 + +**输入 - comparison.json:** +```json +{ + "changes": [{ + "type": "replaced", + "old_line": 66, + "old_content": "| style=\"...\"| 2025-11-25
(Season 12)", + "new_content": "| style=\"...\"| 2026-01-25
(Season 12)" + }] +} +``` + +**中文原文第66行:** +``` +| style="color:#3f6e2d; background-color:#161f0c; border-color:#0d1709"| 2025-11-25
(赛季 12) +``` +**更新后第66行:** +``` +| style="color:#3f6e2d; background-color:#161f0c; border-color:#0d1709"| 2026-01-25
(赛季 12) +``` + +**变更说明:** 日期 `2025-11-25` → `2026-01-25`,但 `赛季 12` 保持中文不变。 + + + ## 注意事项 + +1. **行号一致性**:确保中英文文档行号完全对应,这是长期维护的基础 +2. **保留翻译**:只同步变更内容,不替换已有的中文翻译 +3. **MediaWiki 语法**: + - 表格分隔符 `|-` 位置保持一致 + - 链接格式 `[[页面名|显示文本]]` 不变 + - 样式属性 `style="..."` 不变 +4. **特殊字符**:注意 `
`、` ` 等 HTML 实体 + +## 错误处理 + +- 如果 `has_cn_translation` 为 false,提示用户该页面无中文翻译 +- 如果 `is_new_page` 为 true,说明是新页面,需要全新翻译 +- 如果找不到对应行,可能是中英文版本不同步,需要人工确认 \ No newline at end of file diff --git a/.claude/skills/wiki-sync-translate/references/PatchString.txt b/.claude/skills/wiki-sync-translate/references/PatchString.txt new file mode 100644 index 0000000..e856a04 Binary files /dev/null and b/.claude/skills/wiki-sync-translate/references/PatchString.txt differ diff --git a/.claude/skills/wiki-sync-translate/scripts/wiki_sync.py b/.claude/skills/wiki-sync-translate/scripts/wiki_sync.py new file mode 100644 index 0000000..a0f3a30 --- /dev/null +++ b/.claude/skills/wiki-sync-translate/scripts/wiki_sync.py @@ -0,0 +1,417 @@ +# -*- coding: utf-8 -*- +""" +MediaWiki Wiki 同步工具 - AI Agent 版本 +输出 JSON 格式的对比文件,便于 AI Agent 读取和处理 +""" + +import os +import argparse +from pathlib import Path +from datetime import datetime, timedelta +import requests +from dotenv import load_dotenv +import difflib +import json +import re + +# ==================== 配置区 ==================== +load_dotenv() +WIKI_API_URL_EN = os.getenv("WIKI_API_URL_EN", "https://wiki.projectdiablo2.com/w/api.php") +WIKI_API_URL_CN = os.getenv("WIKI_API_URL_CN", "https://wiki.projectdiablo2.cn/w/api.php") +OUTPUT_DIR = Path("wiki_sync_output") +OUTPUT_DIR.mkdir(exist_ok=True) + +CURRENT_OUTPUT_DIR = None +LAST_TIMESTAMP_FILE = "last_sync_timestamp.txt" + +SESSION_EN = requests.Session() +SESSION_EN.headers.update({ + "User-Agent": "WikiSyncTool/5.0 (AI Agent Version)" +}) + +SESSION_CN = requests.Session() +SESSION_CN.headers.update({ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" +}) +SESSION_CN.trust_env = False +# ================================================ + +def load_last_timestamp(): + if os.path.exists(LAST_TIMESTAMP_FILE): + with open(LAST_TIMESTAMP_FILE, encoding="utf-8") as f: + return f.read().strip() + return None + +def save_last_timestamp(ts): + with open(LAST_TIMESTAMP_FILE, "w", encoding="utf-8") as f: + f.write(ts) + +def get_recent_changes(since): + """获取自 since 时间后每个页面的最新 revid""" + params = { + "action": "query", + "list": "recentchanges", + "rcprop": "title|ids|timestamp", + "rctype": "edit|new", + "rcdir": "newer", + "rcstart": since, + "rclimit": 500, + "format": "json" + } + latest = {} + while True: + try: + r = SESSION_EN.get(WIKI_API_URL_EN, params=params) + r.raise_for_status() + data = r.json() + if "error" in data: + raise Exception(data["error"]) + for rc in data.get("query", {}).get("recentchanges", []): + latest[rc["title"]] = (rc["revid"], rc["timestamp"]) + if "continue" not in data: + break + params.update(data["continue"]) + except Exception as e: + print(f"获取最近更改时出错: {e}") + break + return latest + +def get_old_revid(title, end_time): + """获取指定时间前的最后一个 revid""" + params = { + "action": "query", + "prop": "revisions", + "titles": title, + "rvprop": "ids|timestamp", + "rvlimit": 1, + "rvdir": "older", + "rvstart": end_time, + "format": "json" + } + try: + r = SESSION_EN.get(WIKI_API_URL_EN, params=params).json() + pages = r["query"]["pages"] + page = next(iter(pages.values())) + if "revisions" in page: + return page["revisions"][0]["revid"] + except Exception as e: + print(f"获取旧版本ID时出错: {e}") + return None + +def get_page_content(wiki_url, session, title, revid=None): + """获取页面完整内容""" + params = { + "action": "query", + "prop": "revisions", + "titles": title, + "rvprop": "content|timestamp|ids", + "rvslots": "main", + "format": "json" + } + if revid: + params["rvstartid"] = revid + params["rvendid"] = revid + + try: + r = session.get(wiki_url, params=params).json() + pages = r["query"]["pages"] + page = next(iter(pages.values())) + if "revisions" in page: + rev = page["revisions"][0] + return rev["slots"]["main"]["*"], rev["timestamp"], rev["revid"] + except Exception as e: + print(f"获取页面内容时出错: {e}") + return None, None, None + +def generate_text_diff(old_text, new_text): + """生成 unified diff 格式""" + if not old_text: + return "新创建页面" + + old_lines = old_text.splitlines(keepends=True) + new_lines = new_text.splitlines(keepends=True) + + return ''.join(difflib.unified_diff(old_lines, new_lines, lineterm='\n')) + +def parse_diff_to_changes(diff_text): + """ + 解析 diff 文本,提取结构化的变更信息 + 返回一个列表,每个元素包含:变更类型、行号、旧内容、新内容 + """ + if not diff_text or diff_text.startswith("新创建页面"): + return [] + + changes = [] + current_old_line = 0 + current_new_line = 0 + in_hunk = False + + for line in diff_text.splitlines(): + if line.startswith('@@'): + match = re.match(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@', line) + if match: + current_old_line = int(match.group(1)) + current_new_line = int(match.group(3)) + in_hunk = True + elif line.startswith('---') or line.startswith('+++'): + continue + elif in_hunk: + if line.startswith('-'): + changes.append({ + "type": "removed", + "old_line": current_old_line, + "new_line": None, + "old_content": line[1:], + "new_content": None + }) + current_old_line += 1 + elif line.startswith('+'): + changes.append({ + "type": "added", + "old_line": None, + "new_line": current_new_line, + "old_content": None, + "new_content": line[1:] + }) + current_new_line += 1 + elif line.startswith(' '): + current_old_line += 1 + current_new_line += 1 + + return changes + +def group_changes_by_line(changes): + """ + 将变更按行号分组,将连续的删除和添加合并为替换操作 + """ + # 先收集所有的删除和添加 + removed_by_line = {} # old_line -> content + added_by_line = {} # new_line -> content + + for c in changes: + if c["type"] == "removed": + removed_by_line[c["old_line"]] = c["old_content"] + elif c["type"] == "added": + added_by_line[c["new_line"]] = c["new_content"] + + # 尝试将删除和添加配对 + grouped = [] + used_added = set() + + for old_line, old_content in sorted(removed_by_line.items()): + # 找一个未使用的添加行来配对 + paired = False + for new_line, new_content in sorted(added_by_line.items()): + if new_line not in used_added: + grouped.append({ + "type": "replaced", + "old_line": old_line, + "new_line": new_line, + "old_content": old_content, + "new_content": new_content + }) + used_added.add(new_line) + paired = True + break + + if not paired: + grouped.append({ + "type": "removed", + "old_line": old_line, + "new_line": None, + "old_content": old_content, + "new_content": None + }) + + # 添加未配对的新增行 + for new_line, new_content in sorted(added_by_line.items()): + if new_line not in used_added: + grouped.append({ + "type": "added", + "old_line": None, + "new_line": new_line, + "old_content": None, + "new_content": new_content + }) + + # 按行号排序 + grouped.sort(key=lambda x: x["old_line"] or x["new_line"] or 0) + + return grouped + +def create_diff_json(title, en_old_content, en_new_content, cn_content): + """ + 创建结构化的 JSON 对比数据(仅包含英文变更,AI自行匹配中文) + """ + # 生成英文 diff + diff_text = generate_text_diff(en_old_content, en_new_content) + + # 解析变更 + raw_changes = parse_diff_to_changes(diff_text) + grouped_changes = group_changes_by_line(raw_changes) + + # 构建输出结构(精简版,不含中文行内容) + result = { + "title": title, + "timestamp": datetime.now().isoformat(), + "is_new_page": diff_text == "新创建页面", + "has_cn_translation": cn_content is not None, + "summary": { + "total_changes": len(grouped_changes), + "replaced": len([c for c in grouped_changes if c["type"] == "replaced"]), + "added": len([c for c in grouped_changes if c["type"] == "added"]), + "removed": len([c for c in grouped_changes if c["type"] == "removed"]) + }, + "changes": grouped_changes + } + + return result + +def save_files(title, diff_json, en_full_text, cn_content, timestamp, revid=None, old_full_text=None): + """保存文件""" + global CURRENT_OUTPUT_DIR + + if CURRENT_OUTPUT_DIR is None: + current_time_str = datetime.now().strftime("%Y%m%d_%H%M%S") + CURRENT_OUTPUT_DIR = OUTPUT_DIR / current_time_str + CURRENT_OUTPUT_DIR.mkdir(exist_ok=True) + (CURRENT_OUTPUT_DIR / "new_pages").mkdir(exist_ok=True) + (CURRENT_OUTPUT_DIR / "changed_pages").mkdir(exist_ok=True) + print(f"创建输出目录: {CURRENT_OUTPUT_DIR}") + + safe_title = "".join(c if c.isalnum() or c in " -_." else "_" for c in title) + time_str = timestamp[:19].replace("-", "").replace(":", "").replace("T", "_") + base_filename = f"{safe_title}-{time_str}-{revid}" if revid else f"{safe_title}-{time_str}" + + is_new_page = diff_json["is_new_page"] + + if is_new_page: + target_dir = CURRENT_OUTPUT_DIR / "new_pages" + print(f" 检测到新页面") + + # 保存英文完整内容 + full_file = target_dir / f"{base_filename}.full.txt" + with open(full_file, "w", encoding="utf-8") as f: + f.write(en_full_text) + print(f" → 已保存: {full_file.relative_to(OUTPUT_DIR)}") + + else: + target_dir = CURRENT_OUTPUT_DIR / "changed_pages" + + # 保存英文完整内容 + full_file = target_dir / f"{base_filename}.full.txt" + with open(full_file, "w", encoding="utf-8") as f: + f.write(en_full_text) + print(f" → 已保存: {full_file.relative_to(OUTPUT_DIR)}") + + # 保存中文内容 + if cn_content: + cn_file = target_dir / f"{base_filename}.cn.txt" + with open(cn_file, "w", encoding="utf-8") as f: + f.write(cn_content) + print(f" → 已保存: {cn_file.relative_to(OUTPUT_DIR)}") + + # 保存 JSON 对比文件(核心输出) + json_file = target_dir / f"{base_filename}.comparison.json" + with open(json_file, "w", encoding="utf-8") as f: + json.dump(diff_json, f, ensure_ascii=False, indent=2) + print(f" → 已保存: {json_file.relative_to(OUTPUT_DIR)} (AI Agent 对比文件)") + + # 保存历史版本 + if old_full_text: + old_file = target_dir / f"{base_filename}.old.txt" + with open(old_file, "w", encoding="utf-8") as f: + f.write(old_full_text) + print(f" → 已保存: {old_file.relative_to(OUTPUT_DIR)}") + +def process_single_page(title, since_time, update_timestamp=False): + """处理单个页面""" + print(f"正在处理页面:{title}") + + # 获取最新内容 + latest_content, latest_ts, latest_revid = get_page_content(WIKI_API_URL_EN, SESSION_EN, title) + if latest_content is None: + print("页面不存在或被删除") + return None + + # 获取旧版本 + old_revid = get_old_revid(title, since_time) + old_content = None + + if old_revid: + old_content, _, _ = get_page_content(WIKI_API_URL_EN, SESSION_EN, title, old_revid) + if old_content is None: + print(" 无法获取历史版本,视为新页面") + + # 获取中文翻译 + print(" 搜索中文翻译...") + cn_content = None + + # 直接尝试获取同名页面 + cn_result, _, _ = get_page_content(WIKI_API_URL_CN, SESSION_CN, title) + if cn_result: + cn_content = cn_result + print(f" 找到中文页面 ({len(cn_content)} 字符)") + else: + print(" 未找到中文翻译") + + # 生成对比 JSON + diff_json = create_diff_json(title, old_content, latest_content, cn_content) + + print(f" 变更统计: 替换={diff_json['summary']['replaced']}, " + f"新增={diff_json['summary']['added']}, 删除={diff_json['summary']['removed']}") + + # 保存文件 + save_files(title, diff_json, latest_content, cn_content, latest_ts, latest_revid, old_content) + + if update_timestamp: + save_last_timestamp(latest_ts) + print(f"已更新时间戳 → {latest_ts}") + + return latest_ts + +def process_all_pages_since(since_time): + """处理所有变更页面""" + print("正在获取最近变更列表...") + changes = get_recent_changes(since_time) + + if not changes: + print("没有发现任何变更") + return + + latest_global_ts = since_time + for title, (revid, ts) in changes.items(): + print(f"\n处理:{title}") + page_ts = process_single_page(title, since_time) + if page_ts and page_ts > latest_global_ts: + latest_global_ts = page_ts + + save_last_timestamp(latest_global_ts) + print(f"\n同步完成!最新时间戳: {latest_global_ts}") + print(f"文件保存在: {CURRENT_OUTPUT_DIR.resolve() if CURRENT_OUTPUT_DIR else OUTPUT_DIR.resolve()}") + +def main(): + parser = argparse.ArgumentParser(description="MediaWiki 同步工具 - AI Agent 版本") + parser.add_argument("--since", type=str, help="起始时间,格式: 2025-11-28T00:00:00Z") + parser.add_argument("--title", type=str, help="只处理指定页面") + parser.add_argument("--update-timestamp", action="store_true", help="更新全局时间戳") + parser.add_argument("--run", action="store_true", help="执行同步") + + args = parser.parse_args() + + if not args.run: + parser.print_help() + return + + since_time = args.since or load_last_timestamp() + if not since_time: + since_time = (datetime.utcnow() - timedelta(days=1)).isoformat(timespec='seconds') + "Z" + print(f"起始时间: {since_time}") + + if args.title: + process_single_page(args.title.strip(), since_time, args.update_timestamp) + else: + process_all_pages_since(since_time) + +if __name__ == "__main__": + main() diff --git a/sync.py b/sync.py index cf189ad..17504bc 100644 --- a/sync.py +++ b/sync.py @@ -41,8 +41,9 @@ SESSION_EN.headers.update({ SESSION_CN = requests.Session() SESSION_CN.headers.update({ - "User-Agent": "WikiSyncTool/4.0 (your-email@example.com; MediaWiki Sync Bot)" + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" }) +SESSION_CN.trust_env = False # 禁用环境变量代理 # ================================================ def load_last_timestamp(): @@ -264,31 +265,24 @@ def parse_diff_with_line_numbers(diff_text): def search_chinese_page(title): """在中文wiki中搜索对应的页面""" - # 首先尝试精确匹配 + # 首先尝试直接获取页面(因为中文wiki禁用了标题搜索) params = { "action": "query", - "list": "search", - "srsearch": f'"{title}"', - "srwhat": "title", - "srlimit": 5, + "prop": "revisions", + "titles": title, + "rvprop": "ids", "format": "json" } try: - r = SESSION_CN.get(WIKI_API_URL_CN, params=params).json() - search_results = r.get("query", {}).get("search", []) + r = SESSION_CN.get(WIKI_API_URL_CN, params=params, timeout=10) + data = r.json() + pages = data.get("query", {}).get("pages", {}) - if search_results: - # 返回第一个匹配的结果 - return search_results[0]["title"] - - # 如果精确匹配没有结果,尝试模糊搜索 - params["srsearch"] = title.replace(" ", "%20") - r = SESSION_CN.get(WIKI_API_URL_CN, params=params).json() - search_results = r.get("query", {}).get("search", []) - - if search_results: - return search_results[0]["title"] + for page_id, page_info in pages.items(): + # page_id 为负数表示页面不存在 + if page_id != "-1" and "missing" not in page_info: + return page_info.get("title") except Exception as e: print(f"搜索中文页面时出错: {e}")