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 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.12.0",
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": {
@@ -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": {