md2wiki 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,180 @@
1
+ <div align="center">
2
+ <br />
3
+ <img src="public/logo.svg" alt="md2wiki logo" width="160" />
4
+ <br />
5
+ <h1>๐Ÿ”„ MD2WIKI</h1>
6
+ <p><strong>Real-Time Markdown to MediaWiki Wikitext Compiler</strong></p>
7
+ <p><i>A premium, high-density Next.js utility for Wikipedia editors featuring SUL OAuth 2.0 Sandbox publishing and CORS-free repository README importing.</i></p>
8
+
9
+ <br />
10
+
11
+ <div align="center">
12
+ <img src="https://img.shields.io/badge/Stack-Next.js%2015%20%7C%20Tailwind%20v4-emerald?style=for-the-badge" alt="Stack" />
13
+ <img src="https://img.shields.io/badge/License-MIT-blue?style=for-the-badge" alt="License" />
14
+ <img src="https://img.shields.io/badge/Platform-Wikimedia%20Toolforge-violet?style=for-the-badge" alt="Platform" />
15
+ </div>
16
+
17
+ <br />
18
+ <div align="center">
19
+ <img src="assets/preview.png" alt="Showcase Preview" width="600" style="border-radius: 12px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);" />
20
+ </div>
21
+ <br />
22
+ </div>
23
+
24
+ ---
25
+
26
+ ## ๐Ÿงญ Navigation
27
+
28
+ * [๐Ÿ“ The Project Vision](#-the-project-vision)
29
+ * [โœจ Key Features](#-key-features)
30
+ * [๐Ÿš€ Quick Start (Local Setup)](#-quick-start-local-setup)
31
+ * [๐Ÿ”’ OAuth 2.0 Credentials Setup](#-oauth-20-credentials-setup)
32
+ * [๐Ÿ“ฆ Deployment to Toolforge](#-deployment-to-toolforge)
33
+ * [๐Ÿ“‚ Project Structure](#-project-structure)
34
+ * [๐Ÿงช Running Unit Tests](#-running-unit-tests)
35
+
36
+ ---
37
+
38
+ ## ๐Ÿ“ The Project Vision
39
+
40
+ Writing high-quality drafts or documentation offline in Markdown is standard practice for modern developers. However, publishing these drafts on Wikipedia talk pages, user sandboxes, or Meta-Wiki documentation requires manually converting markdown format to MediaWiki Wikitext markup.
41
+
42
+ **md2wiki** solves this pain point. It serves as:
43
+ 1. **An NPM-ready parser module** (`src/core/parser.ts`) that compiles Markdown to wikitext with smart rules.
44
+ 2. **A SUL-integrated Toolforge service** that allows editors to paste raw Markdown (or fetch direct GitHub/GitLab READMEs) and publish the compiled Wikitext straight to their Wikipedia Sandbox with a single click.
45
+
46
+ ---
47
+
48
+ ## โœจ Key Features
49
+
50
+ <div align="center">
51
+ <table border="0" cellspacing="0" cellpadding="20">
52
+ <tr>
53
+ <td width="300" valign="top" style="border: 1px solid #333; border-radius: 15px; background: rgba(255,255,255,0.02); padding: 15px;">
54
+ <h3>โšก Real-Time Compiler</h3>
55
+ <p>Instant, client-side translation of Markdown headers, text styling, tables, code blocks, lists, and links without network lag.</p>
56
+ </td>
57
+ <td width="300" valign="top" style="border: 1px solid #333; border-radius: 15px; background: rgba(255,255,255,0.02); padding: 15px;">
58
+ <h3>๐Ÿ›ก๏ธ Smart Heading Shift</h3>
59
+ <p>Detects Markdown H1s and auto-shifts headings to avoid duplicate H1 page titles on Wikipedia, conforming strictly to style guidelines.</p>
60
+ </td>
61
+ </tr>
62
+ <tr>
63
+ <td width="300" valign="top" style="border: 1px solid #333; border-radius: 15px; background: rgba(255,255,255,0.02); padding: 15px;">
64
+ <h3>๐Ÿ”— Repository Importer</h3>
65
+ <p>Paste any GitHub or GitLab repository link, and our CORS-free proxy endpoint fetches, parses, and translates the README file.</p>
66
+ </td>
67
+ <td width="300" valign="top" style="border: 1px solid #333; border-radius: 15px; background: rgba(255,255,255,0.02); padding: 15px;">
68
+ <h3>๐Ÿ“ Sandbox SUL Publisher</h3>
69
+ <p>Log in securely via Wikimedia SUL OAuth 2.0 and save wikitext directly to <code>User:&lt;Username&gt;/sandbox</code> on any language Wikipedia.</p>
70
+ </td>
71
+ </tr>
72
+ </table>
73
+ </div>
74
+
75
+ ---
76
+
77
+ ## ๐Ÿš€ Quick Start (Local Setup)
78
+
79
+ Follow these steps to run the application on your local machine:
80
+
81
+ ### 1. Clone the repository
82
+ ```bash
83
+ git clone https://github.com/your-username/md2wiki.git
84
+ cd md2wiki
85
+ ```
86
+
87
+ ### 2. Install dependencies
88
+ ```bash
89
+ npm install
90
+ ```
91
+
92
+ ### 3. Set up environment variables
93
+ Create a `.env` file in the root directory (the application will run in **Mock SUL Mode** automatically if credentials are left blank):
94
+ ```env
95
+ WIKIMEDIA_CLIENT_ID=your_oauth_consumer_token
96
+ WIKIMEDIA_CLIENT_SECRET=your_oauth_secret_token
97
+ SESSION_SECRET=a_long_secure_session_encryption_key
98
+ WIKIMEDIA_REDIRECT_URI=http://localhost:8000/api/auth/callback
99
+ ```
100
+
101
+ ### 4. Start the development server
102
+ ```bash
103
+ npm run dev
104
+ ```
105
+ Open **[http://localhost:8000](http://localhost:8000)** in your browser.
106
+
107
+ ---
108
+
109
+ ## ๐Ÿ”’ OAuth 2.0 Credentials Setup
110
+
111
+ To write to Wikipedia Sandboxes on behalf of users, you must register the tool:
112
+
113
+ 1. Log in to [Meta-Wiki](https://meta.wikimedia.org/).
114
+ 2. Navigate to [Special:OAuthConsumerRegistration/propose/oauth2](https://meta.wikimedia.org/wiki/Special:OAuthConsumerRegistration/propose/oauth2).
115
+ 3. Set the parameters:
116
+ * **OAuth callback URL**: `https://md2wiki.toolforge.org/api/auth/callback` (or `http://localhost:8000/api/auth/callback` for dev).
117
+ * **Grants**: Select **Basic rights** (`basic`), **Edit existing pages** (`edit`), and **Create, edit, and move pages** (`writepage` / `create`).
118
+ * **Allowed pages**: Leave blank to allow editing of all pages (required since sandbox pages reside under user namespaces).
119
+ 4. Submit and record your **Consumer Token** and **Secret Token**.
120
+
121
+ ---
122
+
123
+ ## ๐Ÿ“ฆ Deployment to Toolforge
124
+
125
+ Wikimedia Toolforge supports hosting Node.js Next.js web applications using Kubernetes.
126
+
127
+ ### 1. Log in to Toolforge Bastion
128
+ ```bash
129
+ ssh <username>@login.toolforge.org
130
+ ```
131
+
132
+ ### 2. Initialize the Node webservice
133
+ Ensure your project files are cloned in your Toolforge home directory `$HOME/www/js/`:
134
+ ```bash
135
+ git clone https://github.com/your-username/md2wiki.git www/js
136
+ cd www/js
137
+ npm install
138
+ ```
139
+
140
+ ### 3. Create your production `.env` file
141
+ ```env
142
+ WIKIMEDIA_CLIENT_ID=your_production_consumer_token
143
+ WIKIMEDIA_CLIENT_SECRET=your_production_secret_token
144
+ SESSION_SECRET=another_long_production_secret
145
+ WIKIMEDIA_REDIRECT_URI=https://md2wiki.toolforge.org/api/auth/callback
146
+ NODE_ENV=production
147
+ PORT=8000
148
+ ```
149
+
150
+ ### 4. Build and start the service
151
+ Build the Next.js assets:
152
+ ```bash
153
+ npm run build
154
+ ```
155
+
156
+ Configure Toolforge to start the Next.js production server:
157
+ ```bash
158
+ toolforge webservice node18 start
159
+ ```
160
+ Toolforge automatically routes traffic to port `8000`. Next.js will serve the client pages and API routes at `https://md2wiki.toolforge.org/`.
161
+
162
+ ---
163
+
164
+ ## ๐Ÿ“‚ Project Structure
165
+
166
+ * `src/core/`: The core conversion engine. Contains the TS parser and test files.
167
+ * `src/app/`: The Next.js App Router containing:
168
+ * `api/`: API Route Handlers for SUL OAuth redirects, proxy readmes, and sandbox edits.
169
+ * `utils/`: File helper wrappers (session encryptor, raw README URL resolver).
170
+ * `globals.css` / `layout.tsx` / `page.tsx`: The styling baseline and frontend editor layout.
171
+ * `toolinfo.json`: Toolhub scraper metadata.
172
+
173
+ ---
174
+
175
+ ## ๐Ÿงช Running Unit Tests
176
+
177
+ We use **Vitest** for running our unit tests. To verify the Markdown parser rules:
178
+ ```bash
179
+ npm run test
180
+ ```
@@ -0,0 +1,9 @@
1
+ export interface ConverterOptions {
2
+ shiftHeadings?: boolean;
3
+ wikiLinks?: boolean;
4
+ }
5
+ /**
6
+ * Converts Markdown string into MediaWiki Wikitext.
7
+ * Includes smart heading alignment, link conversion, table rendering, and nested list conversion.
8
+ */
9
+ export declare function markdownToWikitext(markdown: string, options?: ConverterOptions): string;
package/dist/parser.js ADDED
@@ -0,0 +1,228 @@
1
+ import { marked } from 'marked';
2
+ /**
3
+ * Converts Markdown string into MediaWiki Wikitext.
4
+ * Includes smart heading alignment, link conversion, table rendering, and nested list conversion.
5
+ */
6
+ export function markdownToWikitext(markdown, options = {}) {
7
+ if (!markdown)
8
+ return '';
9
+ const tokens = marked.lexer(markdown);
10
+ // Smart Heading Normalization
11
+ // If shiftHeadings is undefined (auto), check if there is an H1 heading (depth === 1) in the token tree.
12
+ let shouldShift = options.shiftHeadings;
13
+ if (shouldShift === undefined) {
14
+ shouldShift = hasH1(tokens);
15
+ }
16
+ const shiftAmount = shouldShift ? 1 : 0;
17
+ function hasH1(tokenList) {
18
+ for (const token of tokenList) {
19
+ if (token.type === 'heading' && token.depth === 1) {
20
+ return true;
21
+ }
22
+ if (token.tokens && hasH1(token.tokens)) {
23
+ return true;
24
+ }
25
+ if (token.items) {
26
+ for (const item of token.items) {
27
+ if (item.tokens && hasH1(item.tokens)) {
28
+ return true;
29
+ }
30
+ }
31
+ }
32
+ }
33
+ return false;
34
+ }
35
+ function walk(tokenList, listPrefix = '') {
36
+ if (!tokenList)
37
+ return '';
38
+ let out = '';
39
+ for (const token of tokenList) {
40
+ switch (token.type) {
41
+ case 'space':
42
+ out += token.raw;
43
+ break;
44
+ case 'heading': {
45
+ const depth = Math.min(6, token.depth + shiftAmount);
46
+ const eq = '='.repeat(depth);
47
+ const content = walk(token.tokens).trim();
48
+ out += `\n${eq} ${content} ${eq}\n`;
49
+ break;
50
+ }
51
+ case 'paragraph': {
52
+ const content = walk(token.tokens);
53
+ out += `\n${content}\n`;
54
+ break;
55
+ }
56
+ case 'text': {
57
+ if (token.tokens) {
58
+ out += walk(token.tokens);
59
+ }
60
+ else {
61
+ out += token.text;
62
+ }
63
+ break;
64
+ }
65
+ case 'strong': {
66
+ out += `'''${walk(token.tokens)}'''`;
67
+ break;
68
+ }
69
+ case 'em': {
70
+ out += `''${walk(token.tokens)}''`;
71
+ break;
72
+ }
73
+ case 'codespan': {
74
+ out += `<code>${token.text}</code>`;
75
+ break;
76
+ }
77
+ case 'code': {
78
+ const lang = token.lang ? ` lang="${token.lang}"` : '';
79
+ out += `\n<syntaxhighlight${lang}>\n${token.text}\n</syntaxhighlight>\n`;
80
+ break;
81
+ }
82
+ case 'hr': {
83
+ out += '\n----\n';
84
+ break;
85
+ }
86
+ case 'br': {
87
+ out += '<br />';
88
+ break;
89
+ }
90
+ case 'del': {
91
+ out += `<s>${walk(token.tokens)}</s>`;
92
+ break;
93
+ }
94
+ case 'blockquote': {
95
+ const content = walk(token.tokens).trim();
96
+ out += `\n<blockquote>\n${content}\n</blockquote>\n`;
97
+ break;
98
+ }
99
+ case 'link': {
100
+ const href = token.href;
101
+ const text = walk(token.tokens).trim() || token.text;
102
+ // Check if Wikipedia URL conversion is enabled
103
+ let isWikiLink = false;
104
+ if (options.wikiLinks !== false) {
105
+ const wpMatch = href.match(/^https?:\/\/([a-z-]+)\.wikipedia\.org\/wiki\/([^#?]+)/);
106
+ if (wpMatch) {
107
+ const pageName = decodeURIComponent(wpMatch[2]).replace(/_/g, ' ');
108
+ if (text === pageName || text === href || text === `https://${wpMatch[1]}.wikipedia.org/wiki/${wpMatch[2]}`) {
109
+ out += `[[${pageName}]]`;
110
+ }
111
+ else {
112
+ out += `[[${pageName}|${text}]]`;
113
+ }
114
+ isWikiLink = true;
115
+ }
116
+ }
117
+ if (!isWikiLink) {
118
+ if (href.startsWith('#')) {
119
+ out += `[[#${href.slice(1)}|${text}]]`;
120
+ }
121
+ else {
122
+ if (text) {
123
+ out += `[${href} ${text}]`;
124
+ }
125
+ else {
126
+ out += `[${href}]`;
127
+ }
128
+ }
129
+ }
130
+ break;
131
+ }
132
+ case 'image': {
133
+ const href = token.href;
134
+ const alt = token.text || '';
135
+ const parts = href.split('/');
136
+ const filename = parts[parts.length - 1] || href;
137
+ if (alt) {
138
+ out += `[[File:${filename}|thumb|alt=${alt}|${alt}]]`;
139
+ }
140
+ else {
141
+ out += `[[File:${filename}|thumb]]`;
142
+ }
143
+ break;
144
+ }
145
+ case 'list': {
146
+ out += '\n' + walkList(token, listPrefix) + '\n';
147
+ break;
148
+ }
149
+ case 'table': {
150
+ out += '\n' + walkTable(token) + '\n';
151
+ break;
152
+ }
153
+ case 'html': {
154
+ out += token.text;
155
+ break;
156
+ }
157
+ case 'escape': {
158
+ out += token.text;
159
+ break;
160
+ }
161
+ default: {
162
+ out += token.raw || '';
163
+ break;
164
+ }
165
+ }
166
+ }
167
+ return out;
168
+ }
169
+ function walkList(listToken, currentPrefix) {
170
+ const symbol = listToken.ordered ? '#' : '*';
171
+ const itemPrefix = currentPrefix + symbol;
172
+ let result = '';
173
+ for (const item of listToken.items) {
174
+ const inlineTokens = [];
175
+ const childLists = [];
176
+ for (const token of item.tokens) {
177
+ if (token.type === 'list') {
178
+ childLists.push(token);
179
+ }
180
+ else {
181
+ inlineTokens.push(token);
182
+ }
183
+ }
184
+ // Render the item's main content
185
+ const content = walk(inlineTokens).trim();
186
+ result += `${itemPrefix} ${content}\n`;
187
+ // Render child lists
188
+ for (const childList of childLists) {
189
+ result += walkList(childList, itemPrefix);
190
+ }
191
+ }
192
+ return result;
193
+ }
194
+ function walkTable(tableToken) {
195
+ let wikitext = '{| class="wikitable"\n';
196
+ // Render Headers
197
+ if (tableToken.header && tableToken.header.length > 0) {
198
+ wikitext += '! ';
199
+ const headers = tableToken.header.map((cell, idx) => {
200
+ const align = tableToken.align[idx];
201
+ const alignAttr = align && align !== 'center' ? `align="${align}" | ` : '';
202
+ return `${alignAttr}${walk(cell.tokens).trim()}`;
203
+ });
204
+ wikitext += headers.join(' !! ') + '\n';
205
+ }
206
+ // Render Rows
207
+ if (tableToken.rows) {
208
+ for (const row of tableToken.rows) {
209
+ wikitext += '|-\n';
210
+ wikitext += '| ';
211
+ const cells = row.map((cell, idx) => {
212
+ const align = tableToken.align[idx];
213
+ const alignAttr = align && align !== 'center' ? `align="${align}" | ` : '';
214
+ return `${alignAttr}${walk(cell.tokens).trim()}`;
215
+ });
216
+ wikitext += cells.join(' || ') + '\n';
217
+ }
218
+ }
219
+ wikitext += '|}';
220
+ return wikitext;
221
+ }
222
+ let result = walk(tokens);
223
+ // Clean up excessive newlines
224
+ result = result
225
+ .replace(/\n{3,}/g, '\n\n')
226
+ .trim();
227
+ return result;
228
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "md2wiki",
3
+ "version": "1.0.0",
4
+ "description": "Convert Markdown to MediaWiki Wikitext with SUL sandbox publishing.",
5
+ "type": "module",
6
+ "main": "./dist/parser.js",
7
+ "types": "./dist/parser.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "dev": "next dev -p 8000",
13
+ "build": "next build",
14
+ "build:lib": "tsc -p tsconfig.lib.json",
15
+ "start": "next start -p 8000",
16
+ "test": "vitest run",
17
+ "test:watch": "vitest",
18
+ "parse": "tsx src/core/cli.ts",
19
+ "prepublishOnly": "npm run build:lib"
20
+ },
21
+ "dependencies": {
22
+ "@tailwindcss/postcss": "^4.3.1",
23
+ "dotenv": "^16.4.5",
24
+ "marked": "^12.0.1",
25
+ "next": "^16.2.9",
26
+ "postcss": "^8.5.15",
27
+ "react": "^19.2.7",
28
+ "react-dom": "^19.2.7"
29
+ },
30
+ "devDependencies": {
31
+ "@tabler/icons-react": "^3.1.0",
32
+ "@tailwindcss/vite": "^4.0.0",
33
+ "@types/node": "^20.11.24",
34
+ "@types/react": "^19.0.0",
35
+ "@types/react-dom": "^19.0.0",
36
+ "tailwindcss": "^4.0.0",
37
+ "tsx": "^4.7.1",
38
+ "typescript": "^5.3.3",
39
+ "vitest": "^1.3.1"
40
+ },
41
+ "license": "MIT"
42
+ }