naider 1.12.0 → 1.14.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 +80 -0
- package/SPEC.naide +64 -0
- package/lsp/server.js +9 -0
- package/package.json +1 -1
- package/src/generator-python.js +293 -0
- package/src/generator.js +323 -0
- package/src/parser.js +214 -1
- package/src/tokens.js +16 -0
- package/vscode-naide/syntaxes/naide.tmLanguage.json +1 -1
package/README.md
CHANGED
|
@@ -676,6 +676,86 @@ watch User.create (event):
|
|
|
676
676
|
Intervals: `"30s"`, `"5m"`, `"1h"`, `"1d"`. Cron expressions auto-detected.
|
|
677
677
|
`watch` connects to `crud` events automatically.
|
|
678
678
|
|
|
679
|
+
## OAuth (Social Login)
|
|
680
|
+
|
|
681
|
+
```python
|
|
682
|
+
oauth "google" env.CLIENT_ID env.CLIENT_SECRET:
|
|
683
|
+
callback "/auth/callback"
|
|
684
|
+
scope "email profile"
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
Supports Google, GitHub. Compiles to passport.js (Node.js) or authlib (Python).
|
|
688
|
+
|
|
689
|
+
## Payment (Stripe)
|
|
690
|
+
|
|
691
|
+
```python
|
|
692
|
+
pay "stripe" env.STRIPE_KEY:
|
|
693
|
+
webhook "/webhook"
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
Usage: `pay.checkout([{name: "Item", price: 1000, qty: 1}])`. Compiles to Stripe SDK.
|
|
697
|
+
|
|
698
|
+
## Cloud Storage (S3/GCS)
|
|
699
|
+
|
|
700
|
+
```python
|
|
701
|
+
storage "s3" env.BUCKET env.AWS_KEY env.AWS_SECRET:
|
|
702
|
+
region "ap-northeast-1"
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
Usage: `storage.upload("key", data)`, `storage.download("key")`, `storage.remove("key")`. Compiles to AWS SDK (Node.js) or boto3 (Python).
|
|
706
|
+
|
|
707
|
+
## PDF Generation
|
|
708
|
+
|
|
709
|
+
```python
|
|
710
|
+
pdf "report.pdf":
|
|
711
|
+
title "Monthly Report"
|
|
712
|
+
h1 "Summary"
|
|
713
|
+
text "This is the content."
|
|
714
|
+
image "chart.png"
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
Compiles to pdfkit (Node.js) or FPDF (Python).
|
|
718
|
+
|
|
719
|
+
## i18n (Internationalization)
|
|
720
|
+
|
|
721
|
+
```python
|
|
722
|
+
i18n "locales/":
|
|
723
|
+
default "en"
|
|
724
|
+
lang "en" "en.json"
|
|
725
|
+
lang "ja" "ja.json"
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
Usage: `i18n.t("greeting.hello")`, `i18n.setLang("ja")`.
|
|
729
|
+
|
|
730
|
+
## Push Notifications
|
|
731
|
+
|
|
732
|
+
```python
|
|
733
|
+
push env.VAPID_PUBLIC env.VAPID_PRIVATE:
|
|
734
|
+
endpoint "/subscribe"
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
Usage: `push.send(subscription, "Title", "Body")`. Compiles to web-push (Node.js) or pywebpush (Python).
|
|
738
|
+
|
|
739
|
+
## Full-Text Search
|
|
740
|
+
|
|
741
|
+
```python
|
|
742
|
+
search "meilisearch" "http://localhost:7700" env.MEILI_KEY:
|
|
743
|
+
index "products"
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
Usage: `search.query("keyword")`, `search.add(docs)`. Supports Meilisearch and Elasticsearch.
|
|
747
|
+
|
|
748
|
+
## Image Processing
|
|
749
|
+
|
|
750
|
+
```python
|
|
751
|
+
image "photo.jpg" -> "output.jpg":
|
|
752
|
+
resize 800 600
|
|
753
|
+
grayscale
|
|
754
|
+
watermark "logo.png"
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
Operations: `resize`, `crop`, `rotate`, `blur`, `grayscale`, `flip`, `watermark`, `format`. Compiles to sharp (Node.js) or Pillow (Python).
|
|
758
|
+
|
|
679
759
|
## Environment Variables
|
|
680
760
|
|
|
681
761
|
```python
|
package/SPEC.naide
CHANGED
|
@@ -520,3 +520,67 @@ screen Home:
|
|
|
520
520
|
button "Click Me"
|
|
521
521
|
input "Enter your name"
|
|
522
522
|
image "logo.png"
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
# ---- OAuth (ソーシャルログイン) ----
|
|
526
|
+
oauth "google" env.CLIENT_ID env.CLIENT_SECRET:
|
|
527
|
+
callback "/auth/callback"
|
|
528
|
+
scope "email profile"
|
|
529
|
+
|
|
530
|
+
oauth "github" env.GH_ID env.GH_SECRET:
|
|
531
|
+
callback "/auth/github/callback"
|
|
532
|
+
scope "user:email"
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
# ---- 決済 (Stripe) ----
|
|
536
|
+
pay "stripe" env.STRIPE_KEY:
|
|
537
|
+
webhook "/webhook"
|
|
538
|
+
success "/thanks"
|
|
539
|
+
cancel "/cancel"
|
|
540
|
+
# usage: pay.checkout([{name: "Item", price: 1000, qty: 1}])
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
# ---- ファイルストレージ (S3/GCS) ----
|
|
544
|
+
storage "s3" env.BUCKET env.AWS_KEY env.AWS_SECRET:
|
|
545
|
+
region "ap-northeast-1"
|
|
546
|
+
# usage: storage.upload("key", data)
|
|
547
|
+
# usage: storage.download("key")
|
|
548
|
+
# usage: storage.remove("key")
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
# ---- PDF生成 ----
|
|
552
|
+
pdf "report.pdf":
|
|
553
|
+
title "Monthly Report"
|
|
554
|
+
h1 "Summary"
|
|
555
|
+
text "This is the report content."
|
|
556
|
+
line
|
|
557
|
+
image "chart.png"
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
# ---- i18n (多言語) ----
|
|
561
|
+
i18n "locales/":
|
|
562
|
+
default "en"
|
|
563
|
+
lang "en" "en.json"
|
|
564
|
+
lang "ja" "ja.json"
|
|
565
|
+
# usage: i18n.t("greeting.hello")
|
|
566
|
+
# usage: i18n.setLang("ja")
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
# ---- Push通知 ----
|
|
570
|
+
push env.VAPID_PUBLIC env.VAPID_PRIVATE:
|
|
571
|
+
endpoint "/subscribe"
|
|
572
|
+
# usage: push.send(subscription, "Title", "Body")
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
# ---- 全文検索 ----
|
|
576
|
+
search "meilisearch" "http://localhost:7700" env.MEILI_KEY:
|
|
577
|
+
index "products"
|
|
578
|
+
# usage: search.query("keyword")
|
|
579
|
+
# usage: search.add([{id: 1, name: "Item"}])
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
# ---- 画像処理 ----
|
|
583
|
+
image "photo.jpg" -> "output.jpg":
|
|
584
|
+
resize 800 600
|
|
585
|
+
grayscale
|
|
586
|
+
watermark "logo.png"
|
package/lsp/server.js
CHANGED
|
@@ -289,6 +289,7 @@ function getCompletions() {
|
|
|
289
289
|
'cors', 'auth', 'crud', 'limit', 'cookie', 'session', 'static', 'ws', 'sse',
|
|
290
290
|
'cache', 'view', 'upload', 'group', 'validate', 'openapi', 'error', 'mid', 'prompt',
|
|
291
291
|
'page', 'cli', 'mail', 'graphql', 'desktop', 'screen',
|
|
292
|
+
'oauth', 'pay', 'storage', 'pdf', 'i18n', 'push', 'search', 'image',
|
|
292
293
|
];
|
|
293
294
|
const builtins = [
|
|
294
295
|
{ label: 'uuid()', detail: 'Generate UUID v4', insertText: 'uuid()' },
|
|
@@ -345,6 +346,14 @@ const HOVER_DOCS = {
|
|
|
345
346
|
'desktop': '**desktop** — Desktop app (Electron/pywebview)\n```naide\ndesktop myApp:\n title "My App"\n size 1024 768\n load "index.html"\n```',
|
|
346
347
|
'screen': '**screen** — Mobile screen (React Native/Kivy)\n```naide\nscreen Home:\n text "Hello World"\n button "Click Me"\n input "Enter name"\n```',
|
|
347
348
|
'every': '**every** — Scheduled task / cron\n```naide\nevery "5s":\n log "tick"\nevery "*/5 * * * *":\n log "cron"\n```',
|
|
349
|
+
'oauth': '**oauth** — Social login (Google/GitHub)\n```naide\noauth "google" env.CLIENT_ID env.CLIENT_SECRET:\n callback "/auth/callback"\n scope "email profile"\n```',
|
|
350
|
+
'pay': '**pay** — Payment (Stripe)\n```naide\npay "stripe" env.STRIPE_KEY:\n webhook "/webhook"\n```\nUsage: `pay.checkout(items, successUrl, cancelUrl)`',
|
|
351
|
+
'storage': '**storage** — Cloud storage (S3/GCS)\n```naide\nstorage "s3" env.BUCKET env.KEY env.SECRET:\n region "ap-northeast-1"\n```\nUsage: `storage.upload(key, body)`, `storage.download(key)`',
|
|
352
|
+
'pdf': '**pdf** — PDF generation\n```naide\npdf "report.pdf":\n title "Report"\n text "Hello"\n```',
|
|
353
|
+
'i18n': '**i18n** — Internationalization\n```naide\ni18n "locales/":\n default "en"\n lang "en" "en.json"\n lang "ja" "ja.json"\n```\nUsage: `i18n.t("key")`, `i18n.setLang("ja")`',
|
|
354
|
+
'push': '**push** — Push notifications\n```naide\npush env.VAPID_PUBLIC env.VAPID_PRIVATE:\n endpoint "/subscribe"\n```\nUsage: `push.send(subscription, title, body)`',
|
|
355
|
+
'search': '**search** — Full-text search\n```naide\nsearch "meilisearch" "http://localhost:7700" env.KEY:\n index "products"\n```\nUsage: `search.query("keyword")`, `search.add(docs)`',
|
|
356
|
+
'image': '**image** — Image processing\n```naide\nimage "input.jpg" -> "output.jpg":\n resize 800 600\n grayscale\n watermark "logo.png"\n```',
|
|
348
357
|
};
|
|
349
358
|
|
|
350
359
|
function getHover(params) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "naider",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "NAIDE - Node AI Development Environment. AI-specialized language with built-in server, auth, DB, SQL, AI/LLM, WebSocket, bots (Discord/Slack/Telegram/LINE), CLI, HTML, email, GraphQL, desktop, mobile, and more — transpiles to Node.js/Python/Bun.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"exports": {
|
package/src/generator-python.js
CHANGED
|
@@ -148,6 +148,14 @@ export class PythonGenerator {
|
|
|
148
148
|
case 'MailConfig': return this.visitMail(node);
|
|
149
149
|
case 'DesktopApp': return this.visitDesktop(node);
|
|
150
150
|
case 'Screen': return this.visitScreen(node);
|
|
151
|
+
case 'OauthDecl': return this.visitOauth(node);
|
|
152
|
+
case 'PayDecl': return this.visitPay(node);
|
|
153
|
+
case 'StorageDecl': return this.visitStorage(node);
|
|
154
|
+
case 'PdfDecl': return this.visitPdf(node);
|
|
155
|
+
case 'I18nDecl': return this.visitI18n(node);
|
|
156
|
+
case 'PushDecl': return this.visitPush(node);
|
|
157
|
+
case 'SearchDecl': return this.visitSearch(node);
|
|
158
|
+
case 'ImageDecl': return this.visitImage(node);
|
|
151
159
|
default:
|
|
152
160
|
this.emit(`# unknown: ${node.type}`);
|
|
153
161
|
}
|
|
@@ -1955,4 +1963,289 @@ export class PythonGenerator {
|
|
|
1955
1963
|
this.indent--;
|
|
1956
1964
|
this.emitRaw('');
|
|
1957
1965
|
}
|
|
1966
|
+
|
|
1967
|
+
// ===== OAuth =====
|
|
1968
|
+
visitOauth(node) {
|
|
1969
|
+
const provider = this.rawString(node.provider);
|
|
1970
|
+
const clientId = this.expr(node.clientId);
|
|
1971
|
+
const clientSecret = this.expr(node.clientSecret);
|
|
1972
|
+
const callback = node.callback ? this.rawString(node.callback) : '/auth/callback';
|
|
1973
|
+
const scope = node.scope ? this.rawString(node.scope) : 'email profile';
|
|
1974
|
+
|
|
1975
|
+
if (provider === 'google') {
|
|
1976
|
+
this.addFromImport('authlib.integrations.flask_client', 'OAuth');
|
|
1977
|
+
} else if (provider === 'github') {
|
|
1978
|
+
this.addFromImport('authlib.integrations.flask_client', 'OAuth');
|
|
1979
|
+
}
|
|
1980
|
+
this.emitRaw('');
|
|
1981
|
+
this.emit(`oauth = OAuth()`);
|
|
1982
|
+
this.emit(`oauth.register(`);
|
|
1983
|
+
this.indent++;
|
|
1984
|
+
this.emit(`name=${JSON.stringify(provider)},`);
|
|
1985
|
+
this.emit(`client_id=${clientId},`);
|
|
1986
|
+
this.emit(`client_secret=${clientSecret},`);
|
|
1987
|
+
if (provider === 'google') {
|
|
1988
|
+
this.emit(`server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',`);
|
|
1989
|
+
this.emit(`client_kwargs={'scope': ${JSON.stringify(scope)}},`);
|
|
1990
|
+
} else if (provider === 'github') {
|
|
1991
|
+
this.emit(`access_token_url='https://github.com/login/oauth/access_token',`);
|
|
1992
|
+
this.emit(`authorize_url='https://github.com/login/oauth/authorize',`);
|
|
1993
|
+
this.emit(`client_kwargs={'scope': ${JSON.stringify(scope)}},`);
|
|
1994
|
+
}
|
|
1995
|
+
this.indent--;
|
|
1996
|
+
this.emit(`)`);
|
|
1997
|
+
this.emitRaw('');
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
// ===== Pay =====
|
|
2001
|
+
visitPay(node) {
|
|
2002
|
+
const provider = this.rawString(node.provider);
|
|
2003
|
+
const secretKey = this.expr(node.secretKey);
|
|
2004
|
+
|
|
2005
|
+
if (provider === 'stripe') {
|
|
2006
|
+
this.addImport('stripe');
|
|
2007
|
+
this.emitRaw('');
|
|
2008
|
+
this.emit(`stripe.api_key = ${secretKey}`);
|
|
2009
|
+
this.emitRaw('');
|
|
2010
|
+
this.emit(`class Pay:`);
|
|
2011
|
+
this.indent++;
|
|
2012
|
+
this.emit(`@staticmethod`);
|
|
2013
|
+
this.emit(`def checkout(items, success_url=None, cancel_url=None):`);
|
|
2014
|
+
this.indent++;
|
|
2015
|
+
this.emit(`line_items = [{'price_data': {'currency': 'usd', 'product_data': {'name': i['name']}, 'unit_amount': i['price']}, 'quantity': i.get('qty', 1)} for i in items]`);
|
|
2016
|
+
this.emit(`return stripe.checkout.Session.create(line_items=line_items, mode='payment', success_url=success_url or 'http://localhost:3000/success', cancel_url=cancel_url or 'http://localhost:3000/cancel')`);
|
|
2017
|
+
this.indent--;
|
|
2018
|
+
this.indent--;
|
|
2019
|
+
this.emit(`pay = Pay()`);
|
|
2020
|
+
} else {
|
|
2021
|
+
this.emit(`# ${provider} payment integration`);
|
|
2022
|
+
this.emit(`pay = None`);
|
|
2023
|
+
}
|
|
2024
|
+
this.emitRaw('');
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
// ===== Storage =====
|
|
2028
|
+
visitStorage(node) {
|
|
2029
|
+
const provider = this.rawString(node.provider);
|
|
2030
|
+
const bucket = this.expr(node.bucket);
|
|
2031
|
+
const accessKey = this.expr(node.accessKey);
|
|
2032
|
+
const secretKey = this.expr(node.secretKey);
|
|
2033
|
+
const region = node.region ? this.rawString(node.region) : 'us-east-1';
|
|
2034
|
+
|
|
2035
|
+
if (provider === 's3') {
|
|
2036
|
+
this.addImport('boto3');
|
|
2037
|
+
this.emitRaw('');
|
|
2038
|
+
this.emit(`__s3 = boto3.client('s3', region_name=${JSON.stringify(region)}, aws_access_key_id=${accessKey}, aws_secret_access_key=${secretKey})`);
|
|
2039
|
+
this.emitRaw('');
|
|
2040
|
+
this.emit(`class Storage:`);
|
|
2041
|
+
this.indent++;
|
|
2042
|
+
this.emit(`@staticmethod`);
|
|
2043
|
+
this.emit(`def upload(key, body):`);
|
|
2044
|
+
this.indent++;
|
|
2045
|
+
this.emit(`__s3.put_object(Bucket=${bucket}, Key=key, Body=body)`);
|
|
2046
|
+
this.indent--;
|
|
2047
|
+
this.emit(`@staticmethod`);
|
|
2048
|
+
this.emit(`def download(key):`);
|
|
2049
|
+
this.indent++;
|
|
2050
|
+
this.emit(`return __s3.get_object(Bucket=${bucket}, Key=key)['Body'].read()`);
|
|
2051
|
+
this.indent--;
|
|
2052
|
+
this.emit(`@staticmethod`);
|
|
2053
|
+
this.emit(`def remove(key):`);
|
|
2054
|
+
this.indent++;
|
|
2055
|
+
this.emit(`__s3.delete_object(Bucket=${bucket}, Key=key)`);
|
|
2056
|
+
this.indent--;
|
|
2057
|
+
this.indent--;
|
|
2058
|
+
this.emit(`storage = Storage()`);
|
|
2059
|
+
} else if (provider === 'gcs') {
|
|
2060
|
+
this.addFromImport('google.cloud', 'storage as gcs_storage');
|
|
2061
|
+
this.emit(`__gcs = gcs_storage.Client()`);
|
|
2062
|
+
this.emit(`__bucket = __gcs.bucket(${bucket})`);
|
|
2063
|
+
this.emit(`class Storage:`);
|
|
2064
|
+
this.indent++;
|
|
2065
|
+
this.emit(`@staticmethod`);
|
|
2066
|
+
this.emit(`def upload(key, body): __bucket.blob(key).upload_from_string(body)`);
|
|
2067
|
+
this.emit(`@staticmethod`);
|
|
2068
|
+
this.emit(`def download(key): return __bucket.blob(key).download_as_bytes()`);
|
|
2069
|
+
this.emit(`@staticmethod`);
|
|
2070
|
+
this.emit(`def remove(key): __bucket.blob(key).delete()`);
|
|
2071
|
+
this.indent--;
|
|
2072
|
+
this.emit(`storage = Storage()`);
|
|
2073
|
+
}
|
|
2074
|
+
this.emitRaw('');
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
// ===== PDF =====
|
|
2078
|
+
visitPdf(node) {
|
|
2079
|
+
const filename = this.rawString(node.filename);
|
|
2080
|
+
this.addFromImport('fpdf', 'FPDF');
|
|
2081
|
+
this.emitRaw('');
|
|
2082
|
+
this.emit(`__pdf = FPDF()`);
|
|
2083
|
+
this.emit(`__pdf.add_page()`);
|
|
2084
|
+
this.emit(`__pdf.set_auto_page_break(auto=True, margin=15)`);
|
|
2085
|
+
|
|
2086
|
+
for (const el of node.elements) {
|
|
2087
|
+
const tag = el.tag;
|
|
2088
|
+
const arg0 = el.args[0] ? this.rawString(el.args[0]) : '';
|
|
2089
|
+
if (tag === 'title') {
|
|
2090
|
+
this.emit(`__pdf.set_font('Helvetica', 'B', 24)`);
|
|
2091
|
+
this.emit(`__pdf.cell(0, 15, ${JSON.stringify(arg0)}, ln=True)`);
|
|
2092
|
+
} else if (tag === 'h1' || tag === 'h2' || tag === 'heading') {
|
|
2093
|
+
const size = tag === 'h1' ? 20 : 16;
|
|
2094
|
+
this.emit(`__pdf.set_font('Helvetica', 'B', ${size})`);
|
|
2095
|
+
this.emit(`__pdf.cell(0, 12, ${JSON.stringify(arg0)}, ln=True)`);
|
|
2096
|
+
} else if (tag === 'text' || tag === 'p') {
|
|
2097
|
+
this.emit(`__pdf.set_font('Helvetica', '', 12)`);
|
|
2098
|
+
this.emit(`__pdf.multi_cell(0, 8, ${JSON.stringify(arg0)})`);
|
|
2099
|
+
} else if (tag === 'image') {
|
|
2100
|
+
this.emit(`__pdf.image(${JSON.stringify(arg0)}, w=100)`);
|
|
2101
|
+
} else if (tag === 'line') {
|
|
2102
|
+
this.emit(`__pdf.line(10, __pdf.get_y(), 200, __pdf.get_y())`);
|
|
2103
|
+
} else {
|
|
2104
|
+
this.emit(`__pdf.set_font('Helvetica', '', 12)`);
|
|
2105
|
+
this.emit(`__pdf.cell(0, 8, ${JSON.stringify(arg0)}, ln=True)`);
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
this.emit(`__pdf.output(${JSON.stringify(filename)})`);
|
|
2110
|
+
this.emit(`print(f"Generated: ${filename}")`);
|
|
2111
|
+
this.emitRaw('');
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
// ===== i18n =====
|
|
2115
|
+
visitI18n(node) {
|
|
2116
|
+
const dir = this.rawString(node.dir);
|
|
2117
|
+
const defaultLang = node.defaultLang ? this.rawString(node.defaultLang) : 'en';
|
|
2118
|
+
|
|
2119
|
+
this.addImport('json');
|
|
2120
|
+
this.addImport('os');
|
|
2121
|
+
this.emitRaw('');
|
|
2122
|
+
this.emit(`__i18n_data = {}`);
|
|
2123
|
+
for (const lang of node.langs) {
|
|
2124
|
+
const code = this.rawString(lang.code);
|
|
2125
|
+
const file = this.rawString(lang.file);
|
|
2126
|
+
this.emit(`with open(os.path.join(${JSON.stringify(dir)}, ${JSON.stringify(file)})) as f:`);
|
|
2127
|
+
this.indent++;
|
|
2128
|
+
this.emit(`__i18n_data[${JSON.stringify(code)}] = json.load(f)`);
|
|
2129
|
+
this.indent--;
|
|
2130
|
+
}
|
|
2131
|
+
this.emit(`__i18n_lang = ${JSON.stringify(defaultLang)}`);
|
|
2132
|
+
this.emitRaw('');
|
|
2133
|
+
this.emit(`class I18n:`);
|
|
2134
|
+
this.indent++;
|
|
2135
|
+
this.emit(`@staticmethod`);
|
|
2136
|
+
this.emit(`def t(key, **params):`);
|
|
2137
|
+
this.indent++;
|
|
2138
|
+
this.emit(`data = __i18n_data.get(__i18n_lang, {})`);
|
|
2139
|
+
this.emit(`for k in key.split('.'): data = data.get(k, key) if isinstance(data, dict) else key`);
|
|
2140
|
+
this.emit(`text = str(data)`);
|
|
2141
|
+
this.emit(`for k, v in params.items(): text = text.replace('{' + k + '}', str(v))`);
|
|
2142
|
+
this.emit(`return text`);
|
|
2143
|
+
this.indent--;
|
|
2144
|
+
this.emit(`@staticmethod`);
|
|
2145
|
+
this.emit(`def set_lang(code): global __i18n_lang; __i18n_lang = code`);
|
|
2146
|
+
this.emit(`@staticmethod`);
|
|
2147
|
+
this.emit(`def get_lang(): return __i18n_lang`);
|
|
2148
|
+
this.indent--;
|
|
2149
|
+
this.emit(`i18n = I18n()`);
|
|
2150
|
+
this.emitRaw('');
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
// ===== Push =====
|
|
2154
|
+
visitPush(node) {
|
|
2155
|
+
const publicKey = this.expr(node.publicKey);
|
|
2156
|
+
const privateKey = this.expr(node.privateKey);
|
|
2157
|
+
this.addFromImport('pywebpush', 'webpush');
|
|
2158
|
+
this.emitRaw('');
|
|
2159
|
+
this.emit(`class Push:`);
|
|
2160
|
+
this.indent++;
|
|
2161
|
+
this.emit(`VAPID_PUBLIC = ${publicKey}`);
|
|
2162
|
+
this.emit(`VAPID_PRIVATE = ${privateKey}`);
|
|
2163
|
+
this.emit(`@staticmethod`);
|
|
2164
|
+
this.emit(`def send(subscription, title, body):`);
|
|
2165
|
+
this.indent++;
|
|
2166
|
+
this.emit(`import json`);
|
|
2167
|
+
this.emit(`webpush(subscription_info=subscription, data=json.dumps({'title': title, 'body': body}), vapid_private_key=Push.VAPID_PRIVATE, vapid_claims={'sub': 'mailto:noreply@example.com'})`);
|
|
2168
|
+
this.indent--;
|
|
2169
|
+
this.indent--;
|
|
2170
|
+
this.emit(`push = Push()`);
|
|
2171
|
+
this.emitRaw('');
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
// ===== Search =====
|
|
2175
|
+
visitSearch(node) {
|
|
2176
|
+
const engine = this.rawString(node.engine);
|
|
2177
|
+
const host = this.expr(node.host);
|
|
2178
|
+
const apiKey = this.expr(node.apiKey);
|
|
2179
|
+
const index = node.index ? this.rawString(node.index) : 'default';
|
|
2180
|
+
|
|
2181
|
+
if (engine === 'meilisearch') {
|
|
2182
|
+
this.addImport('meilisearch');
|
|
2183
|
+
this.emitRaw('');
|
|
2184
|
+
this.emit(`__search_client = meilisearch.Client(${host}, ${apiKey})`);
|
|
2185
|
+
this.emit(`__search_index = __search_client.index(${JSON.stringify(index)})`);
|
|
2186
|
+
this.emitRaw('');
|
|
2187
|
+
this.emit(`class Search:`);
|
|
2188
|
+
this.indent++;
|
|
2189
|
+
this.emit(`@staticmethod`);
|
|
2190
|
+
this.emit(`def query(q, **opts): return __search_index.search(q, opts)`);
|
|
2191
|
+
this.emit(`@staticmethod`);
|
|
2192
|
+
this.emit(`def add(docs): return __search_index.add_documents(docs)`);
|
|
2193
|
+
this.emit(`@staticmethod`);
|
|
2194
|
+
this.emit(`def remove(doc_id): return __search_index.delete_document(doc_id)`);
|
|
2195
|
+
this.indent--;
|
|
2196
|
+
} else {
|
|
2197
|
+
this.addFromImport('elasticsearch', 'Elasticsearch');
|
|
2198
|
+
this.emitRaw('');
|
|
2199
|
+
this.emit(`__es = Elasticsearch(${host}, api_key=${apiKey})`);
|
|
2200
|
+
this.emitRaw('');
|
|
2201
|
+
this.emit(`class Search:`);
|
|
2202
|
+
this.indent++;
|
|
2203
|
+
this.emit(`@staticmethod`);
|
|
2204
|
+
this.emit(`def query(q, **opts): return __es.search(index=${JSON.stringify(index)}, query={'match': {'_all': q}}, **opts)`);
|
|
2205
|
+
this.emit(`@staticmethod`);
|
|
2206
|
+
this.emit(`def add(doc): return __es.index(index=${JSON.stringify(index)}, body=doc)`);
|
|
2207
|
+
this.emit(`@staticmethod`);
|
|
2208
|
+
this.emit(`def remove(doc_id): return __es.delete(index=${JSON.stringify(index)}, id=doc_id)`);
|
|
2209
|
+
this.indent--;
|
|
2210
|
+
}
|
|
2211
|
+
this.emit(`search = Search()`);
|
|
2212
|
+
this.emitRaw('');
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
// ===== Image =====
|
|
2216
|
+
visitImage(node) {
|
|
2217
|
+
const input = this.expr(node.input);
|
|
2218
|
+
const output = node.output ? this.expr(node.output) : input;
|
|
2219
|
+
this.addFromImport('PIL', 'Image as PILImage');
|
|
2220
|
+
this.emitRaw('');
|
|
2221
|
+
this.emit(`__img = PILImage.open(${input})`);
|
|
2222
|
+
|
|
2223
|
+
for (const op of node.operations) {
|
|
2224
|
+
if (op.op === 'resize') {
|
|
2225
|
+
const w = op.args[0] || 800;
|
|
2226
|
+
const h = op.args[1] || 600;
|
|
2227
|
+
this.emit(`__img = __img.resize((${w}, ${h}))`);
|
|
2228
|
+
} else if (op.op === 'crop') {
|
|
2229
|
+
const l = op.args[0] || 0, t = op.args[1] || 0, r = op.args[2] || 100, b = op.args[3] || 100;
|
|
2230
|
+
this.emit(`__img = __img.crop((${l}, ${t}, ${r}, ${b}))`);
|
|
2231
|
+
} else if (op.op === 'rotate') {
|
|
2232
|
+
this.emit(`__img = __img.rotate(${op.args[0] || 90})`);
|
|
2233
|
+
} else if (op.op === 'blur') {
|
|
2234
|
+
this.addFromImport('PIL.ImageFilter', 'GaussianBlur');
|
|
2235
|
+
this.emit(`__img = __img.filter(GaussianBlur(radius=${op.args[0] || 5}))`);
|
|
2236
|
+
} else if (op.op === 'grayscale' || op.op === 'greyscale') {
|
|
2237
|
+
this.emit(`__img = __img.convert('L')`);
|
|
2238
|
+
} else if (op.op === 'flip') {
|
|
2239
|
+
this.emit(`__img = __img.transpose(PILImage.FLIP_TOP_BOTTOM)`);
|
|
2240
|
+
} else if (op.op === 'watermark') {
|
|
2241
|
+
const wm = op.args[0] ? this.rawString(op.args[0]) : 'watermark.png';
|
|
2242
|
+
this.emit(`__wm = PILImage.open(${JSON.stringify(wm)})`);
|
|
2243
|
+
this.emit(`__img.paste(__wm, (0, 0), __wm)`);
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
this.emit(`__img.save(${output})`);
|
|
2248
|
+
this.emit(`print(f"Processed: {${output}}")`);
|
|
2249
|
+
this.emitRaw('');
|
|
2250
|
+
}
|
|
1958
2251
|
}
|
package/src/generator.js
CHANGED
|
@@ -148,6 +148,14 @@ export class Generator {
|
|
|
148
148
|
case 'MailConfig': return this.visitMail(node);
|
|
149
149
|
case 'DesktopApp': return this.visitDesktop(node);
|
|
150
150
|
case 'Screen': return this.visitScreen(node);
|
|
151
|
+
case 'OauthDecl': return this.visitOauth(node);
|
|
152
|
+
case 'PayDecl': return this.visitPay(node);
|
|
153
|
+
case 'StorageDecl': return this.visitStorage(node);
|
|
154
|
+
case 'PdfDecl': return this.visitPdf(node);
|
|
155
|
+
case 'I18nDecl': return this.visitI18n(node);
|
|
156
|
+
case 'PushDecl': return this.visitPush(node);
|
|
157
|
+
case 'SearchDecl': return this.visitSearch(node);
|
|
158
|
+
case 'ImageDecl': return this.visitImage(node);
|
|
151
159
|
default:
|
|
152
160
|
this.emit(`/* unknown: ${node.type} */`);
|
|
153
161
|
}
|
|
@@ -1848,4 +1856,319 @@ export class Generator {
|
|
|
1848
1856
|
this.indent = savedIndent;
|
|
1849
1857
|
return result;
|
|
1850
1858
|
}
|
|
1859
|
+
|
|
1860
|
+
// ===== OAuth =====
|
|
1861
|
+
|
|
1862
|
+
visitOauth(node) {
|
|
1863
|
+
const provider = this.rawPageString(node.provider);
|
|
1864
|
+
const clientId = this.expr(node.clientId);
|
|
1865
|
+
const clientSecret = this.expr(node.clientSecret);
|
|
1866
|
+
const callback = node.callback ? this.rawPageString(node.callback) : '/auth/callback';
|
|
1867
|
+
const scope = node.scope ? this.rawPageString(node.scope) : 'email profile';
|
|
1868
|
+
|
|
1869
|
+
if (provider === 'google') {
|
|
1870
|
+
this.emit(`import passport from 'passport';`);
|
|
1871
|
+
this.emit(`import { Strategy as GoogleStrategy } from 'passport-google-oauth20';`);
|
|
1872
|
+
this.emitRaw('');
|
|
1873
|
+
this.emit(`passport.use(new GoogleStrategy({`);
|
|
1874
|
+
this.indent++;
|
|
1875
|
+
this.emit(`clientID: ${clientId},`);
|
|
1876
|
+
this.emit(`clientSecret: ${clientSecret},`);
|
|
1877
|
+
this.emit(`callbackURL: ${JSON.stringify(callback)},`);
|
|
1878
|
+
this.indent--;
|
|
1879
|
+
this.emit(`}, (accessToken, refreshToken, profile, done) => done(null, profile)));`);
|
|
1880
|
+
} else if (provider === 'github') {
|
|
1881
|
+
this.emit(`import passport from 'passport';`);
|
|
1882
|
+
this.emit(`import { Strategy as GitHubStrategy } from 'passport-github2';`);
|
|
1883
|
+
this.emitRaw('');
|
|
1884
|
+
this.emit(`passport.use(new GitHubStrategy({`);
|
|
1885
|
+
this.indent++;
|
|
1886
|
+
this.emit(`clientID: ${clientId},`);
|
|
1887
|
+
this.emit(`clientSecret: ${clientSecret},`);
|
|
1888
|
+
this.emit(`callbackURL: ${JSON.stringify(callback)},`);
|
|
1889
|
+
this.indent--;
|
|
1890
|
+
this.emit(`}, (accessToken, refreshToken, profile, done) => done(null, profile)));`);
|
|
1891
|
+
} else {
|
|
1892
|
+
this.emit(`import passport from 'passport';`);
|
|
1893
|
+
this.emit(`// Configure ${provider} OAuth strategy`);
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
this.emit(`passport.serializeUser((user, done) => done(null, user));`);
|
|
1897
|
+
this.emit(`passport.deserializeUser((user, done) => done(null, user));`);
|
|
1898
|
+
this.emitRaw('');
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
// ===== Pay (Stripe) =====
|
|
1902
|
+
|
|
1903
|
+
visitPay(node) {
|
|
1904
|
+
const provider = this.rawPageString(node.provider);
|
|
1905
|
+
const secretKey = this.expr(node.secretKey);
|
|
1906
|
+
const webhook = node.webhook ? this.rawPageString(node.webhook) : '/webhook';
|
|
1907
|
+
|
|
1908
|
+
if (provider === 'stripe') {
|
|
1909
|
+
this.emit(`import Stripe from 'stripe';`);
|
|
1910
|
+
this.emit(`const stripe = new Stripe(${secretKey});`);
|
|
1911
|
+
this.emitRaw('');
|
|
1912
|
+
this.emit(`const pay = {`);
|
|
1913
|
+
this.indent++;
|
|
1914
|
+
this.emit(`async checkout(items, successUrl, cancelUrl) {`);
|
|
1915
|
+
this.indent++;
|
|
1916
|
+
this.emit(`return stripe.checkout.sessions.create({`);
|
|
1917
|
+
this.indent++;
|
|
1918
|
+
this.emit(`line_items: items.map(i => ({ price_data: { currency: 'usd', product_data: { name: i.name }, unit_amount: i.price }, quantity: i.qty || 1 })),`);
|
|
1919
|
+
this.emit(`mode: 'payment',`);
|
|
1920
|
+
this.emit(`success_url: successUrl || ${node.successUrl ? this.expr(node.successUrl) : "'http://localhost:3000/success'"},`);
|
|
1921
|
+
this.emit(`cancel_url: cancelUrl || ${node.cancelUrl ? this.expr(node.cancelUrl) : "'http://localhost:3000/cancel'"},`);
|
|
1922
|
+
this.indent--;
|
|
1923
|
+
this.emit(`});`);
|
|
1924
|
+
this.indent--;
|
|
1925
|
+
this.emit(`},`);
|
|
1926
|
+
this.emit(`async verify(body, sig) {`);
|
|
1927
|
+
this.indent++;
|
|
1928
|
+
this.emit(`return stripe.webhooks.constructEvent(body, sig, ${secretKey});`);
|
|
1929
|
+
this.indent--;
|
|
1930
|
+
this.emit(`},`);
|
|
1931
|
+
this.indent--;
|
|
1932
|
+
this.emit(`};`);
|
|
1933
|
+
} else {
|
|
1934
|
+
this.emit(`// ${provider} payment integration`);
|
|
1935
|
+
this.emit(`const pay = {};`);
|
|
1936
|
+
}
|
|
1937
|
+
this.emitRaw('');
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
// ===== Storage (S3) =====
|
|
1941
|
+
|
|
1942
|
+
visitStorage(node) {
|
|
1943
|
+
const provider = this.rawPageString(node.provider);
|
|
1944
|
+
const bucket = this.expr(node.bucket);
|
|
1945
|
+
const accessKey = this.expr(node.accessKey);
|
|
1946
|
+
const secretKey = this.expr(node.secretKey);
|
|
1947
|
+
const region = node.region ? this.rawPageString(node.region) : 'us-east-1';
|
|
1948
|
+
|
|
1949
|
+
if (provider === 's3') {
|
|
1950
|
+
this.emit(`import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';`);
|
|
1951
|
+
this.emitRaw('');
|
|
1952
|
+
this.emit(`const __s3 = new S3Client({`);
|
|
1953
|
+
this.indent++;
|
|
1954
|
+
this.emit(`region: ${JSON.stringify(region)},`);
|
|
1955
|
+
this.emit(`credentials: { accessKeyId: ${accessKey}, secretAccessKey: ${secretKey} },`);
|
|
1956
|
+
this.indent--;
|
|
1957
|
+
this.emit(`});`);
|
|
1958
|
+
this.emitRaw('');
|
|
1959
|
+
this.emit(`const storage = {`);
|
|
1960
|
+
this.indent++;
|
|
1961
|
+
this.emit(`async upload(key, body, contentType = 'application/octet-stream') {`);
|
|
1962
|
+
this.indent++;
|
|
1963
|
+
this.emit(`return __s3.send(new PutObjectCommand({ Bucket: ${bucket}, Key: key, Body: body, ContentType: contentType }));`);
|
|
1964
|
+
this.indent--;
|
|
1965
|
+
this.emit(`},`);
|
|
1966
|
+
this.emit(`async download(key) {`);
|
|
1967
|
+
this.indent++;
|
|
1968
|
+
this.emit(`const res = await __s3.send(new GetObjectCommand({ Bucket: ${bucket}, Key: key }));`);
|
|
1969
|
+
this.emit(`return res.Body;`);
|
|
1970
|
+
this.indent--;
|
|
1971
|
+
this.emit(`},`);
|
|
1972
|
+
this.emit(`async remove(key) {`);
|
|
1973
|
+
this.indent++;
|
|
1974
|
+
this.emit(`return __s3.send(new DeleteObjectCommand({ Bucket: ${bucket}, Key: key }));`);
|
|
1975
|
+
this.indent--;
|
|
1976
|
+
this.emit(`},`);
|
|
1977
|
+
this.indent--;
|
|
1978
|
+
this.emit(`};`);
|
|
1979
|
+
} else if (provider === 'gcs') {
|
|
1980
|
+
this.emit(`import { Storage } from '@google-cloud/storage';`);
|
|
1981
|
+
this.emit(`const __gcs = new Storage();`);
|
|
1982
|
+
this.emit(`const __bucket = __gcs.bucket(${bucket});`);
|
|
1983
|
+
this.emit(`const storage = {`);
|
|
1984
|
+
this.indent++;
|
|
1985
|
+
this.emit(`async upload(key, body) { await __bucket.file(key).save(body); },`);
|
|
1986
|
+
this.emit(`async download(key) { const [buf] = await __bucket.file(key).download(); return buf; },`);
|
|
1987
|
+
this.emit(`async remove(key) { await __bucket.file(key).delete(); },`);
|
|
1988
|
+
this.indent--;
|
|
1989
|
+
this.emit(`};`);
|
|
1990
|
+
} else {
|
|
1991
|
+
this.emit(`// ${provider} storage integration`);
|
|
1992
|
+
this.emit(`const storage = {};`);
|
|
1993
|
+
}
|
|
1994
|
+
this.emitRaw('');
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
// ===== PDF =====
|
|
1998
|
+
|
|
1999
|
+
visitPdf(node) {
|
|
2000
|
+
const filename = this.rawPageString(node.filename);
|
|
2001
|
+
this.emit(`import PDFDocument from 'pdfkit';`);
|
|
2002
|
+
this.emit(`import { createWriteStream } from 'fs';`);
|
|
2003
|
+
this.emitRaw('');
|
|
2004
|
+
this.emit(`const __pdf = new PDFDocument();`);
|
|
2005
|
+
this.emit(`__pdf.pipe(createWriteStream(${JSON.stringify(filename)}));`);
|
|
2006
|
+
|
|
2007
|
+
for (const el of node.elements) {
|
|
2008
|
+
const tag = el.tag;
|
|
2009
|
+
const arg0 = el.args[0] ? (el.args[0].raw !== undefined ? JSON.stringify(el.args[0].raw || el.args[0].parts?.map(p => p.value).join('')) : this.expr(el.args[0])) : '""';
|
|
2010
|
+
if (tag === 'title') {
|
|
2011
|
+
this.emit(`__pdf.fontSize(24).text(${arg0});`);
|
|
2012
|
+
this.emit(`__pdf.moveDown();`);
|
|
2013
|
+
} else if (tag === 'heading' || tag === 'h1' || tag === 'h2') {
|
|
2014
|
+
const size = tag === 'h1' ? 20 : 16;
|
|
2015
|
+
this.emit(`__pdf.fontSize(${size}).text(${arg0});`);
|
|
2016
|
+
this.emit(`__pdf.moveDown();`);
|
|
2017
|
+
} else if (tag === 'text' || tag === 'p') {
|
|
2018
|
+
this.emit(`__pdf.fontSize(12).text(${arg0});`);
|
|
2019
|
+
} else if (tag === 'image') {
|
|
2020
|
+
this.emit(`__pdf.image(${arg0}, { width: 300 });`);
|
|
2021
|
+
} else if (tag === 'line') {
|
|
2022
|
+
this.emit(`__pdf.moveTo(50, __pdf.y).lineTo(550, __pdf.y).stroke();`);
|
|
2023
|
+
this.emit(`__pdf.moveDown();`);
|
|
2024
|
+
} else if (tag === 'table') {
|
|
2025
|
+
this.emit(`// table: ${arg0}`);
|
|
2026
|
+
} else {
|
|
2027
|
+
this.emit(`__pdf.text(${arg0});`);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
this.emit(`__pdf.end();`);
|
|
2032
|
+
this.emit(`console.log('Generated: ${filename}');`);
|
|
2033
|
+
this.emitRaw('');
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
// ===== i18n =====
|
|
2037
|
+
|
|
2038
|
+
visitI18n(node) {
|
|
2039
|
+
const dir = this.rawPageString(node.dir);
|
|
2040
|
+
const defaultLang = node.defaultLang ? this.rawPageString(node.defaultLang) : 'en';
|
|
2041
|
+
|
|
2042
|
+
this.emit(`import { readFileSync } from 'fs';`);
|
|
2043
|
+
this.emit(`import { join } from 'path';`);
|
|
2044
|
+
this.emitRaw('');
|
|
2045
|
+
this.emit(`const __i18nData = {};`);
|
|
2046
|
+
for (const lang of node.langs) {
|
|
2047
|
+
const code = this.rawPageString(lang.code);
|
|
2048
|
+
const file = this.rawPageString(lang.file);
|
|
2049
|
+
this.emit(`__i18nData[${JSON.stringify(code)}] = JSON.parse(readFileSync(join(${JSON.stringify(dir)}, ${JSON.stringify(file)}), 'utf-8'));`);
|
|
2050
|
+
}
|
|
2051
|
+
this.emit(`let __i18nLang = ${JSON.stringify(defaultLang)};`);
|
|
2052
|
+
this.emitRaw('');
|
|
2053
|
+
this.emit(`const i18n = {`);
|
|
2054
|
+
this.indent++;
|
|
2055
|
+
this.emit(`t(key, params = {}) {`);
|
|
2056
|
+
this.indent++;
|
|
2057
|
+
this.emit(`let text = key.split('.').reduce((o, k) => o?.[k], __i18nData[__i18nLang]) || key;`);
|
|
2058
|
+
this.emit(`for (const [k, v] of Object.entries(params)) text = text.replace(new RegExp(\`{$\{k}}\`, 'g'), v);`);
|
|
2059
|
+
this.emit(`return text;`);
|
|
2060
|
+
this.indent--;
|
|
2061
|
+
this.emit(`},`);
|
|
2062
|
+
this.emit(`setLang(code) { __i18nLang = code; },`);
|
|
2063
|
+
this.emit(`getLang() { return __i18nLang; },`);
|
|
2064
|
+
this.indent--;
|
|
2065
|
+
this.emit(`};`);
|
|
2066
|
+
this.emitRaw('');
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
// ===== Push Notifications =====
|
|
2070
|
+
|
|
2071
|
+
visitPush(node) {
|
|
2072
|
+
const publicKey = this.expr(node.publicKey);
|
|
2073
|
+
const privateKey = this.expr(node.privateKey);
|
|
2074
|
+
const endpoint = node.endpoint ? this.rawPageString(node.endpoint) : '/subscribe';
|
|
2075
|
+
|
|
2076
|
+
this.emit(`import webpush from 'web-push';`);
|
|
2077
|
+
this.emitRaw('');
|
|
2078
|
+
this.emit(`webpush.setVapidDetails('mailto:noreply@example.com', ${publicKey}, ${privateKey});`);
|
|
2079
|
+
this.emitRaw('');
|
|
2080
|
+
this.emit(`const push = {`);
|
|
2081
|
+
this.indent++;
|
|
2082
|
+
this.emit(`async send(subscription, title, body, data = {}) {`);
|
|
2083
|
+
this.indent++;
|
|
2084
|
+
this.emit(`return webpush.sendNotification(subscription, JSON.stringify({ title, body, data }));`);
|
|
2085
|
+
this.indent--;
|
|
2086
|
+
this.emit(`},`);
|
|
2087
|
+
this.emit(`async sendAll(subscriptions, title, body, data = {}) {`);
|
|
2088
|
+
this.indent++;
|
|
2089
|
+
this.emit(`return Promise.allSettled(subscriptions.map(sub => push.send(sub, title, body, data)));`);
|
|
2090
|
+
this.indent--;
|
|
2091
|
+
this.emit(`},`);
|
|
2092
|
+
this.indent--;
|
|
2093
|
+
this.emit(`};`);
|
|
2094
|
+
this.emitRaw('');
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
// ===== Search =====
|
|
2098
|
+
|
|
2099
|
+
visitSearch(node) {
|
|
2100
|
+
const engine = this.rawPageString(node.engine);
|
|
2101
|
+
const host = this.expr(node.host);
|
|
2102
|
+
const apiKey = this.expr(node.apiKey);
|
|
2103
|
+
const index = node.index ? this.rawPageString(node.index) : 'default';
|
|
2104
|
+
|
|
2105
|
+
if (engine === 'meilisearch') {
|
|
2106
|
+
this.emit(`import { MeiliSearch } from 'meilisearch';`);
|
|
2107
|
+
this.emitRaw('');
|
|
2108
|
+
this.emit(`const __searchClient = new MeiliSearch({ host: ${host}, apiKey: ${apiKey} });`);
|
|
2109
|
+
this.emit(`const __searchIndex = __searchClient.index(${JSON.stringify(index)});`);
|
|
2110
|
+
this.emitRaw('');
|
|
2111
|
+
this.emit(`const search = {`);
|
|
2112
|
+
this.indent++;
|
|
2113
|
+
this.emit(`async query(q, opts = {}) { return __searchIndex.search(q, opts); },`);
|
|
2114
|
+
this.emit(`async add(docs) { return __searchIndex.addDocuments(docs); },`);
|
|
2115
|
+
this.emit(`async remove(id) { return __searchIndex.deleteDocument(id); },`);
|
|
2116
|
+
this.emit(`async update(docs) { return __searchIndex.updateDocuments(docs); },`);
|
|
2117
|
+
this.indent--;
|
|
2118
|
+
this.emit(`};`);
|
|
2119
|
+
} else {
|
|
2120
|
+
this.emit(`import { Client } from '@elastic/elasticsearch';`);
|
|
2121
|
+
this.emitRaw('');
|
|
2122
|
+
this.emit(`const __esClient = new Client({ node: ${host}, auth: { apiKey: ${apiKey} } });`);
|
|
2123
|
+
this.emitRaw('');
|
|
2124
|
+
this.emit(`const search = {`);
|
|
2125
|
+
this.indent++;
|
|
2126
|
+
this.emit(`async query(q, opts = {}) { return __esClient.search({ index: ${JSON.stringify(index)}, query: { match: { _all: q } }, ...opts }); },`);
|
|
2127
|
+
this.emit(`async add(doc) { return __esClient.index({ index: ${JSON.stringify(index)}, body: doc }); },`);
|
|
2128
|
+
this.emit(`async remove(id) { return __esClient.delete({ index: ${JSON.stringify(index)}, id }); },`);
|
|
2129
|
+
this.indent--;
|
|
2130
|
+
this.emit(`};`);
|
|
2131
|
+
}
|
|
2132
|
+
this.emitRaw('');
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
// ===== Image Processing =====
|
|
2136
|
+
|
|
2137
|
+
visitImage(node) {
|
|
2138
|
+
const input = this.expr(node.input);
|
|
2139
|
+
const output = node.output ? this.expr(node.output) : input;
|
|
2140
|
+
|
|
2141
|
+
this.emit(`import sharp from 'sharp';`);
|
|
2142
|
+
this.emitRaw('');
|
|
2143
|
+
this.emit(`let __img = sharp(${input});`);
|
|
2144
|
+
|
|
2145
|
+
for (const op of node.operations) {
|
|
2146
|
+
if (op.op === 'resize') {
|
|
2147
|
+
const w = op.args[0] || 800;
|
|
2148
|
+
const h = op.args[1] || null;
|
|
2149
|
+
this.emit(`__img = __img.resize(${w}${h ? ', ' + h : ''});`);
|
|
2150
|
+
} else if (op.op === 'crop') {
|
|
2151
|
+
const l = op.args[0] || 0, t = op.args[1] || 0, w = op.args[2] || 100, h = op.args[3] || 100;
|
|
2152
|
+
this.emit(`__img = __img.extract({ left: ${l}, top: ${t}, width: ${w}, height: ${h} });`);
|
|
2153
|
+
} else if (op.op === 'watermark') {
|
|
2154
|
+
const wm = op.args[0] ? (typeof op.args[0] === 'object' ? this.rawPageString(op.args[0]) : op.args[0]) : 'watermark.png';
|
|
2155
|
+
this.emit(`__img = __img.composite([{ input: ${JSON.stringify(wm)}, gravity: 'southeast' }]);`);
|
|
2156
|
+
} else if (op.op === 'rotate') {
|
|
2157
|
+
this.emit(`__img = __img.rotate(${op.args[0] || 90});`);
|
|
2158
|
+
} else if (op.op === 'blur') {
|
|
2159
|
+
this.emit(`__img = __img.blur(${op.args[0] || 5});`);
|
|
2160
|
+
} else if (op.op === 'grayscale' || op.op === 'greyscale') {
|
|
2161
|
+
this.emit(`__img = __img.grayscale();`);
|
|
2162
|
+
} else if (op.op === 'flip') {
|
|
2163
|
+
this.emit(`__img = __img.flip();`);
|
|
2164
|
+
} else if (op.op === 'format') {
|
|
2165
|
+
const fmt = op.args[0] ? (typeof op.args[0] === 'object' ? this.rawPageString(op.args[0]) : op.args[0]) : 'png';
|
|
2166
|
+
this.emit(`__img = __img.toFormat(${JSON.stringify(fmt)});`);
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
this.emit(`await __img.toFile(${output});`);
|
|
2171
|
+
this.emit(`console.log('Processed:', ${output});`);
|
|
2172
|
+
this.emitRaw('');
|
|
2173
|
+
}
|
|
1851
2174
|
}
|
package/src/parser.js
CHANGED
|
@@ -111,7 +111,10 @@ export class Parser {
|
|
|
111
111
|
type === T.TYPEOF || type === T.INSTANCEOF || type === T.ENSURE ||
|
|
112
112
|
type === T.MODEL || type === T.PROMPT ||
|
|
113
113
|
type === T.PAGE || type === T.CLI_APP || type === T.MAIL ||
|
|
114
|
-
type === T.GRAPHQL || type === T.DESKTOP || type === T.SCREEN
|
|
114
|
+
type === T.GRAPHQL || type === T.DESKTOP || type === T.SCREEN ||
|
|
115
|
+
type === T.OAUTH || type === T.PAY || type === T.STORAGE ||
|
|
116
|
+
type === T.PDF || type === T.I18N ||
|
|
117
|
+
type === T.PUSH || type === T.SEARCH || type === T.IMAGE;
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
expectPropertyName() {
|
|
@@ -173,6 +176,14 @@ export class Parser {
|
|
|
173
176
|
case T.MAIL: return this.parseMail();
|
|
174
177
|
case T.DESKTOP: return this.parseDesktop();
|
|
175
178
|
case T.SCREEN: return this.parseScreen();
|
|
179
|
+
case T.OAUTH: return this.parseOauth();
|
|
180
|
+
case T.PAY: return this.parsePay();
|
|
181
|
+
case T.STORAGE: return this.parseStorage();
|
|
182
|
+
case T.PDF: return this.parsePdf();
|
|
183
|
+
case T.I18N: return this.parseI18n();
|
|
184
|
+
case T.PUSH: return this.parsePush();
|
|
185
|
+
case T.SEARCH: return this.parseSearch();
|
|
186
|
+
case T.IMAGE: return this.parseImage();
|
|
176
187
|
case T.MODEL: return this.parseModel();
|
|
177
188
|
case T.ON: return this.parseOn();
|
|
178
189
|
case T.LOG: return this.parseLog();
|
|
@@ -1148,6 +1159,8 @@ export class Parser {
|
|
|
1148
1159
|
case T.PROMPT:
|
|
1149
1160
|
case T.PAGE: case T.CLI_APP: case T.MAIL:
|
|
1150
1161
|
case T.GRAPHQL: case T.DESKTOP: case T.SCREEN:
|
|
1162
|
+
case T.OAUTH: case T.PAY: case T.STORAGE: case T.PDF: case T.I18N:
|
|
1163
|
+
case T.PUSH: case T.SEARCH: case T.IMAGE:
|
|
1151
1164
|
case T.FROM: case T.AS: case T.IN:
|
|
1152
1165
|
this.advance();
|
|
1153
1166
|
return new ASTNode('Identifier', { name: tok.value });
|
|
@@ -1935,4 +1948,204 @@ export class Parser {
|
|
|
1935
1948
|
}
|
|
1936
1949
|
return new ASTNode('ScreenElement', { tag, args, body });
|
|
1937
1950
|
}
|
|
1951
|
+
|
|
1952
|
+
// oauth "google" clientId clientSecret:
|
|
1953
|
+
// callback "/auth/callback"
|
|
1954
|
+
// scope "email profile"
|
|
1955
|
+
parseOauth() {
|
|
1956
|
+
this.expect(T.OAUTH);
|
|
1957
|
+
const provider = this.parseString();
|
|
1958
|
+
const clientId = this.parseExpression();
|
|
1959
|
+
const clientSecret = this.parseExpression();
|
|
1960
|
+
this.expect(T.COLON);
|
|
1961
|
+
this.skipNewlines();
|
|
1962
|
+
this.expect(T.INDENT);
|
|
1963
|
+
let callback = null, scope = null;
|
|
1964
|
+
while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
|
|
1965
|
+
this.skipNewlines();
|
|
1966
|
+
if (this.at(T.DEDENT) || this.at(T.EOF)) break;
|
|
1967
|
+
const kw = this.peek().value;
|
|
1968
|
+
if (kw === 'callback') { this.advance(); callback = this.parseString(); }
|
|
1969
|
+
else if (kw === 'scope') { this.advance(); scope = this.parseString(); }
|
|
1970
|
+
else { this.advance(); }
|
|
1971
|
+
this.skipNewlines();
|
|
1972
|
+
}
|
|
1973
|
+
if (this.at(T.DEDENT)) this.advance();
|
|
1974
|
+
return new ASTNode('OauthDecl', { provider, clientId, clientSecret, callback, scope });
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
// pay "stripe" secretKey:
|
|
1978
|
+
// webhook "/webhook"
|
|
1979
|
+
parsePay() {
|
|
1980
|
+
this.expect(T.PAY);
|
|
1981
|
+
const provider = this.parseString();
|
|
1982
|
+
const secretKey = this.parseExpression();
|
|
1983
|
+
this.expect(T.COLON);
|
|
1984
|
+
this.skipNewlines();
|
|
1985
|
+
this.expect(T.INDENT);
|
|
1986
|
+
let webhook = null, successUrl = null, cancelUrl = null;
|
|
1987
|
+
while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
|
|
1988
|
+
this.skipNewlines();
|
|
1989
|
+
if (this.at(T.DEDENT) || this.at(T.EOF)) break;
|
|
1990
|
+
const kw = this.peek().value;
|
|
1991
|
+
if (kw === 'webhook') { this.advance(); webhook = this.parseString(); }
|
|
1992
|
+
else if (kw === 'success') { this.advance(); successUrl = this.parseString(); }
|
|
1993
|
+
else if (kw === 'cancel') { this.advance(); cancelUrl = this.parseString(); }
|
|
1994
|
+
else { this.advance(); }
|
|
1995
|
+
this.skipNewlines();
|
|
1996
|
+
}
|
|
1997
|
+
if (this.at(T.DEDENT)) this.advance();
|
|
1998
|
+
return new ASTNode('PayDecl', { provider, secretKey, webhook, successUrl, cancelUrl });
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
// storage "s3" bucket accessKey secretKey:
|
|
2002
|
+
// region "ap-northeast-1"
|
|
2003
|
+
parseStorage() {
|
|
2004
|
+
this.expect(T.STORAGE);
|
|
2005
|
+
const provider = this.parseString();
|
|
2006
|
+
const bucket = this.parseExpression();
|
|
2007
|
+
const accessKey = this.parseExpression();
|
|
2008
|
+
const secretKey = this.parseExpression();
|
|
2009
|
+
this.expect(T.COLON);
|
|
2010
|
+
this.skipNewlines();
|
|
2011
|
+
this.expect(T.INDENT);
|
|
2012
|
+
let region = null;
|
|
2013
|
+
while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
|
|
2014
|
+
this.skipNewlines();
|
|
2015
|
+
if (this.at(T.DEDENT) || this.at(T.EOF)) break;
|
|
2016
|
+
const kw = this.peek().value;
|
|
2017
|
+
if (kw === 'region') { this.advance(); region = this.parseString(); }
|
|
2018
|
+
else { this.advance(); }
|
|
2019
|
+
this.skipNewlines();
|
|
2020
|
+
}
|
|
2021
|
+
if (this.at(T.DEDENT)) this.advance();
|
|
2022
|
+
return new ASTNode('StorageDecl', { provider, bucket, accessKey, secretKey, region });
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
// pdf "output.pdf":
|
|
2026
|
+
// title "My Document"
|
|
2027
|
+
// text "Hello World"
|
|
2028
|
+
// table data
|
|
2029
|
+
parsePdf() {
|
|
2030
|
+
this.expect(T.PDF);
|
|
2031
|
+
const filename = this.parseString();
|
|
2032
|
+
this.expect(T.COLON);
|
|
2033
|
+
this.skipNewlines();
|
|
2034
|
+
this.expect(T.INDENT);
|
|
2035
|
+
const elements = [];
|
|
2036
|
+
while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
|
|
2037
|
+
this.skipNewlines();
|
|
2038
|
+
if (this.at(T.DEDENT) || this.at(T.EOF)) break;
|
|
2039
|
+
const tag = this.advance().value;
|
|
2040
|
+
const args = [];
|
|
2041
|
+
while (this.at(T.STRING)) args.push(this.parseString());
|
|
2042
|
+
if (args.length === 0 && !this.at(T.NEWLINE) && !this.at(T.DEDENT) && !this.at(T.EOF)) {
|
|
2043
|
+
args.push(this.parseExpression());
|
|
2044
|
+
}
|
|
2045
|
+
elements.push({ tag, args });
|
|
2046
|
+
this.skipNewlines();
|
|
2047
|
+
}
|
|
2048
|
+
if (this.at(T.DEDENT)) this.advance();
|
|
2049
|
+
return new ASTNode('PdfDecl', { filename, elements });
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
// i18n "locales/":
|
|
2053
|
+
// default "en"
|
|
2054
|
+
// lang "ja" "jp.json"
|
|
2055
|
+
// lang "en" "en.json"
|
|
2056
|
+
parseI18n() {
|
|
2057
|
+
this.expect(T.I18N);
|
|
2058
|
+
const dir = this.parseString();
|
|
2059
|
+
this.expect(T.COLON);
|
|
2060
|
+
this.skipNewlines();
|
|
2061
|
+
this.expect(T.INDENT);
|
|
2062
|
+
let defaultLang = null;
|
|
2063
|
+
const langs = [];
|
|
2064
|
+
while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
|
|
2065
|
+
this.skipNewlines();
|
|
2066
|
+
if (this.at(T.DEDENT) || this.at(T.EOF)) break;
|
|
2067
|
+
const kw = this.peek().value;
|
|
2068
|
+
if (kw === 'default') { this.advance(); defaultLang = this.parseString(); }
|
|
2069
|
+
else if (kw === 'lang') { this.advance(); const code = this.parseString(); const file = this.parseString(); langs.push({ code, file }); }
|
|
2070
|
+
else { this.advance(); }
|
|
2071
|
+
this.skipNewlines();
|
|
2072
|
+
}
|
|
2073
|
+
if (this.at(T.DEDENT)) this.advance();
|
|
2074
|
+
return new ASTNode('I18nDecl', { dir, defaultLang, langs });
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
// push "vapid_public" "vapid_private":
|
|
2078
|
+
// endpoint "/subscribe"
|
|
2079
|
+
parsePush() {
|
|
2080
|
+
this.expect(T.PUSH);
|
|
2081
|
+
const publicKey = this.parseExpression();
|
|
2082
|
+
const privateKey = this.parseExpression();
|
|
2083
|
+
this.expect(T.COLON);
|
|
2084
|
+
this.skipNewlines();
|
|
2085
|
+
this.expect(T.INDENT);
|
|
2086
|
+
let endpoint = null;
|
|
2087
|
+
while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
|
|
2088
|
+
this.skipNewlines();
|
|
2089
|
+
if (this.at(T.DEDENT) || this.at(T.EOF)) break;
|
|
2090
|
+
const kw = this.peek().value;
|
|
2091
|
+
if (kw === 'endpoint') { this.advance(); endpoint = this.parseString(); }
|
|
2092
|
+
else { this.advance(); }
|
|
2093
|
+
this.skipNewlines();
|
|
2094
|
+
}
|
|
2095
|
+
if (this.at(T.DEDENT)) this.advance();
|
|
2096
|
+
return new ASTNode('PushDecl', { publicKey, privateKey, endpoint });
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
// search "meilisearch" "http://localhost:7700" apiKey:
|
|
2100
|
+
// index "products"
|
|
2101
|
+
parseSearch() {
|
|
2102
|
+
this.expect(T.SEARCH);
|
|
2103
|
+
const engine = this.parseString();
|
|
2104
|
+
const host = this.parseExpression();
|
|
2105
|
+
const apiKey = this.parseExpression();
|
|
2106
|
+
this.expect(T.COLON);
|
|
2107
|
+
this.skipNewlines();
|
|
2108
|
+
this.expect(T.INDENT);
|
|
2109
|
+
let index = null;
|
|
2110
|
+
while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
|
|
2111
|
+
this.skipNewlines();
|
|
2112
|
+
if (this.at(T.DEDENT) || this.at(T.EOF)) break;
|
|
2113
|
+
const kw = this.peek().value;
|
|
2114
|
+
if (kw === 'index') { this.advance(); index = this.parseString(); }
|
|
2115
|
+
else { this.advance(); }
|
|
2116
|
+
this.skipNewlines();
|
|
2117
|
+
}
|
|
2118
|
+
if (this.at(T.DEDENT)) this.advance();
|
|
2119
|
+
return new ASTNode('SearchDecl', { engine, host, apiKey, index });
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
// image "input.jpg" -> "output.jpg":
|
|
2123
|
+
// resize 800 600
|
|
2124
|
+
// crop 100 100 400 300
|
|
2125
|
+
// watermark "logo.png"
|
|
2126
|
+
parseImage() {
|
|
2127
|
+
this.expect(T.IMAGE);
|
|
2128
|
+
const input = this.parseExpression();
|
|
2129
|
+
let output = null;
|
|
2130
|
+
if (this.at(T.ARROW)) { this.advance(); output = this.parseExpression(); }
|
|
2131
|
+
this.expect(T.COLON);
|
|
2132
|
+
this.skipNewlines();
|
|
2133
|
+
this.expect(T.INDENT);
|
|
2134
|
+
const operations = [];
|
|
2135
|
+
while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
|
|
2136
|
+
this.skipNewlines();
|
|
2137
|
+
if (this.at(T.DEDENT) || this.at(T.EOF)) break;
|
|
2138
|
+
const op = this.advance().value;
|
|
2139
|
+
const args = [];
|
|
2140
|
+
while (!this.at(T.NEWLINE) && !this.at(T.DEDENT) && !this.at(T.EOF)) {
|
|
2141
|
+
if (this.at(T.STRING)) { args.push(this.parseString()); }
|
|
2142
|
+
else if (this.at(T.NUMBER)) { args.push(this.advance().value); }
|
|
2143
|
+
else { args.push(this.parseExpression()); break; }
|
|
2144
|
+
}
|
|
2145
|
+
operations.push({ op, args });
|
|
2146
|
+
this.skipNewlines();
|
|
2147
|
+
}
|
|
2148
|
+
if (this.at(T.DEDENT)) this.advance();
|
|
2149
|
+
return new ASTNode('ImageDecl', { input, output, operations });
|
|
2150
|
+
}
|
|
1938
2151
|
}
|
package/src/tokens.js
CHANGED
|
@@ -93,6 +93,14 @@ export const T = {
|
|
|
93
93
|
GRAPHQL: 'GRAPHQL',
|
|
94
94
|
DESKTOP: 'DESKTOP',
|
|
95
95
|
SCREEN: 'SCREEN',
|
|
96
|
+
OAUTH: 'OAUTH',
|
|
97
|
+
PAY: 'PAY',
|
|
98
|
+
STORAGE: 'STORAGE',
|
|
99
|
+
PDF: 'PDF',
|
|
100
|
+
I18N: 'I18N',
|
|
101
|
+
PUSH: 'PUSH',
|
|
102
|
+
SEARCH: 'SEARCH',
|
|
103
|
+
IMAGE: 'IMAGE',
|
|
96
104
|
|
|
97
105
|
// Operators
|
|
98
106
|
ASSIGN: 'ASSIGN',
|
|
@@ -214,6 +222,14 @@ export const KEYWORDS = {
|
|
|
214
222
|
'graphql': T.GRAPHQL,
|
|
215
223
|
'desktop': T.DESKTOP,
|
|
216
224
|
'screen': T.SCREEN,
|
|
225
|
+
'oauth': T.OAUTH,
|
|
226
|
+
'pay': T.PAY,
|
|
227
|
+
'storage': T.STORAGE,
|
|
228
|
+
'pdf': T.PDF,
|
|
229
|
+
'i18n': T.I18N,
|
|
230
|
+
'push': T.PUSH,
|
|
231
|
+
'search': T.SEARCH,
|
|
232
|
+
'image': T.IMAGE,
|
|
217
233
|
'true': T.BOOL,
|
|
218
234
|
'false': T.BOOL,
|
|
219
235
|
'null': T.NULL,
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"name": "keyword.control.naide"
|
|
88
88
|
},
|
|
89
89
|
"keywords-server": {
|
|
90
|
-
"match": "\\b(server|bot|slash|get|post|put|del|patch|mid|cors|limit|auth|crud|static|ws|sse|group|upload|cookie|session|view|cache|validate|openapi|error|on|page|cli|mail|graphql|desktop|screen)\\b",
|
|
90
|
+
"match": "\\b(server|bot|slash|get|post|put|del|patch|mid|cors|limit|auth|crud|static|ws|sse|group|upload|cookie|session|view|cache|validate|openapi|error|on|page|cli|mail|graphql|desktop|screen|oauth|pay|storage|pdf|i18n|push|search|image)\\b",
|
|
91
91
|
"name": "keyword.other.naide"
|
|
92
92
|
},
|
|
93
93
|
"keywords-declaration": {
|