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 +180 -0
- package/dist/parser.d.ts +9 -0
- package/dist/parser.js +228 -0
- package/package.json +42 -0
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:<Username>/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
|
+
```
|
package/dist/parser.d.ts
ADDED
|
@@ -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
|
+
}
|