aecode 0.2.0__py3-none-any.whl
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.
- aecode/__init__.py +7 -0
- aecode/api_engine.py +161 -0
- aecode/cli.py +61 -0
- aecode/flow_engine.py +138 -0
- aecode/ui_engine.py +661 -0
- aecode/web_engine.py +85 -0
- aecode-0.2.0.dist-info/METADATA +214 -0
- aecode-0.2.0.dist-info/RECORD +12 -0
- aecode-0.2.0.dist-info/WHEEL +5 -0
- aecode-0.2.0.dist-info/entry_points.txt +2 -0
- aecode-0.2.0.dist-info/licenses/LICENSE +21 -0
- aecode-0.2.0.dist-info/top_level.txt +1 -0
aecode/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""AeCode - DSL ultra-compacto para scraping, UI y APIs."""
|
|
2
|
+
__version__ = "0.2.0"
|
|
3
|
+
|
|
4
|
+
from .web_engine import ae_get, ae_select, ae_attr, ae_crawl, scrape
|
|
5
|
+
from .ui_engine import build_ui, build_and_open
|
|
6
|
+
from .flow_engine import run_flow
|
|
7
|
+
from .api_engine import ae_api_get, ae_api_post, ae_api_graphql, run_api
|
aecode/api_engine.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
def _resolve_nested_key(data, key_path):
|
|
6
|
+
if not isinstance(data, dict):
|
|
7
|
+
return None
|
|
8
|
+
keys = key_path.strip().split('.')
|
|
9
|
+
curr = data
|
|
10
|
+
for k in keys:
|
|
11
|
+
if isinstance(curr, dict) and k in curr:
|
|
12
|
+
curr = curr[k]
|
|
13
|
+
elif isinstance(curr, list) and k.isdigit():
|
|
14
|
+
idx = int(k)
|
|
15
|
+
if 0 <= idx < len(curr):
|
|
16
|
+
curr = curr[idx]
|
|
17
|
+
else:
|
|
18
|
+
return None
|
|
19
|
+
else:
|
|
20
|
+
return None
|
|
21
|
+
return curr
|
|
22
|
+
|
|
23
|
+
def ae_api_get(url, headers=None, params=None):
|
|
24
|
+
hdrs = {'User-Agent': 'AeCode/0.2.0'}
|
|
25
|
+
if headers:
|
|
26
|
+
hdrs.update(headers)
|
|
27
|
+
resp = requests.get(url, headers=hdrs, params=params, timeout=15)
|
|
28
|
+
resp.raise_for_status()
|
|
29
|
+
return resp.json()
|
|
30
|
+
|
|
31
|
+
def ae_api_post(url, body=None, headers=None):
|
|
32
|
+
hdrs = {'User-Agent': 'AeCode/0.2.0', 'Content-Type': 'application/json'}
|
|
33
|
+
if headers:
|
|
34
|
+
hdrs.update(headers)
|
|
35
|
+
resp = requests.post(url, headers=hdrs, json=body or {}, timeout=15)
|
|
36
|
+
resp.raise_for_status()
|
|
37
|
+
return resp.json()
|
|
38
|
+
|
|
39
|
+
def ae_api_graphql(url, query, variables=None, headers=None):
|
|
40
|
+
hdrs = {'User-Agent': 'AeCode/0.2.0', 'Content-Type': 'application/json'}
|
|
41
|
+
if headers:
|
|
42
|
+
hdrs.update(headers)
|
|
43
|
+
payload = {'query': query}
|
|
44
|
+
if variables:
|
|
45
|
+
payload['variables'] = variables
|
|
46
|
+
resp = requests.post(url, headers=hdrs, json=payload, timeout=15)
|
|
47
|
+
resp.raise_for_status()
|
|
48
|
+
return resp.json()
|
|
49
|
+
|
|
50
|
+
def ae_api_select(data, fields):
|
|
51
|
+
if isinstance(data, list):
|
|
52
|
+
results = []
|
|
53
|
+
for item in data:
|
|
54
|
+
row = {}
|
|
55
|
+
for f in fields:
|
|
56
|
+
alias = f
|
|
57
|
+
key = f
|
|
58
|
+
if ' as ' in f.lower():
|
|
59
|
+
parts = re.split(r'\s+as\s+', f, flags=re.IGNORECASE)
|
|
60
|
+
key = parts[0].strip()
|
|
61
|
+
alias = parts[1].strip()
|
|
62
|
+
row[alias] = _resolve_nested_key(item, key)
|
|
63
|
+
results.append(row)
|
|
64
|
+
return results
|
|
65
|
+
elif isinstance(data, dict):
|
|
66
|
+
row = {}
|
|
67
|
+
for f in fields:
|
|
68
|
+
alias = f
|
|
69
|
+
key = f
|
|
70
|
+
if ' as ' in f.lower():
|
|
71
|
+
parts = re.split(r'\s+as\s+', f, flags=re.IGNORECASE)
|
|
72
|
+
key = parts[0].strip()
|
|
73
|
+
alias = parts[1].strip()
|
|
74
|
+
row[alias] = _resolve_nested_key(data, key)
|
|
75
|
+
return row
|
|
76
|
+
return data
|
|
77
|
+
|
|
78
|
+
def run_api(code):
|
|
79
|
+
"""
|
|
80
|
+
Executes an AeAPI script.
|
|
81
|
+
Syntax:
|
|
82
|
+
DATA = GET "url" | SELECT field1, field2
|
|
83
|
+
DATA = POST "url" {"key": "val"} | SELECT ...
|
|
84
|
+
DATA = GRAPHQL "url" "query" | SELECT ...
|
|
85
|
+
"""
|
|
86
|
+
env = {}
|
|
87
|
+
lines = [l.strip() for l in code.strip().split('\n') if l.strip() and not l.strip().startswith('//')]
|
|
88
|
+
|
|
89
|
+
for line in lines:
|
|
90
|
+
var_name = "RESULT"
|
|
91
|
+
pipeline = line
|
|
92
|
+
if '=' in line and not line.startswith('GET') and not line.startswith('POST') and not line.startswith('GRAPHQL'):
|
|
93
|
+
parts = line.split('=', 1)
|
|
94
|
+
var_name = parts[0].strip()
|
|
95
|
+
pipeline = parts[1].strip()
|
|
96
|
+
|
|
97
|
+
segments = [s.strip() for s in pipeline.split('|')]
|
|
98
|
+
source_segment = segments[0]
|
|
99
|
+
current_data = None
|
|
100
|
+
|
|
101
|
+
# 1. Source (GET, POST, GRAPHQL)
|
|
102
|
+
m_get = re.match(r'GET\s+"([^"]+)"(?:\s+(\{.*?\}))?', source_segment)
|
|
103
|
+
m_post = re.match(r'POST\s+"([^"]+)"(?:\s+(\{.*?\}))?', source_segment)
|
|
104
|
+
m_gql = re.match(r'GRAPHQL\s+"([^"]+)"\s+"([^"]+)"(?:\s+(\{.*?\}))?', source_segment)
|
|
105
|
+
|
|
106
|
+
if m_get:
|
|
107
|
+
url = m_get.group(1)
|
|
108
|
+
raw_headers = m_get.group(2)
|
|
109
|
+
headers = json.loads(raw_headers) if raw_headers else None
|
|
110
|
+
current_data = ae_api_get(url, headers=headers)
|
|
111
|
+
elif m_post:
|
|
112
|
+
url = m_post.group(1)
|
|
113
|
+
raw_body = m_post.group(2)
|
|
114
|
+
body = json.loads(raw_body) if raw_body else {}
|
|
115
|
+
current_data = ae_api_post(url, body=body)
|
|
116
|
+
elif m_gql:
|
|
117
|
+
url = m_gql.group(1)
|
|
118
|
+
query = m_gql.group(2)
|
|
119
|
+
raw_vars = m_gql.group(3)
|
|
120
|
+
variables = json.loads(raw_vars) if raw_vars else None
|
|
121
|
+
current_data = ae_api_graphql(url, query=query, variables=variables)
|
|
122
|
+
|
|
123
|
+
# 2. Pipeline processing (| SELECT ..., | filter(...))
|
|
124
|
+
for op in segments[1:]:
|
|
125
|
+
op = op.strip()
|
|
126
|
+
if op.startswith('SELECT'):
|
|
127
|
+
fields_str = op[len('SELECT'):].strip()
|
|
128
|
+
fields = [f.strip() for f in fields_str.split(',') if f.strip()]
|
|
129
|
+
current_data = ae_api_select(current_data, fields)
|
|
130
|
+
elif op.startswith('filter('):
|
|
131
|
+
m_filter = re.search(r'filter\((.*?)\)', op)
|
|
132
|
+
if m_filter and isinstance(current_data, list):
|
|
133
|
+
expr = m_filter.group(1)
|
|
134
|
+
# Simple filter like field > val or field == val
|
|
135
|
+
m_cond = re.match(r'([a-zA-Z0-9_]+)\s*(==|!=|>|<|>=|<=)\s*(.*)', expr)
|
|
136
|
+
if m_cond:
|
|
137
|
+
f_name, op_sym, target_val = m_cond.groups()
|
|
138
|
+
target_val = target_val.strip().strip('"').strip("'")
|
|
139
|
+
try:
|
|
140
|
+
target_val = float(target_val)
|
|
141
|
+
except:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
def matches(item):
|
|
145
|
+
val = item.get(f_name)
|
|
146
|
+
if val is None:
|
|
147
|
+
return False
|
|
148
|
+
if op_sym == '==': return val == target_val
|
|
149
|
+
if op_sym == '!=': return val != target_val
|
|
150
|
+
if op_sym == '>': return val > target_val
|
|
151
|
+
if op_sym == '<': return val < target_val
|
|
152
|
+
if op_sym == '>=': return val >= target_val
|
|
153
|
+
if op_sym == '<=': return val <= target_val
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
current_data = [x for x in current_data if matches(x)]
|
|
157
|
+
|
|
158
|
+
env[var_name] = current_data
|
|
159
|
+
print(f"[{var_name}] => {json.dumps(current_data, indent=2, ensure_ascii=False)}")
|
|
160
|
+
|
|
161
|
+
return env
|
aecode/cli.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
@click.group()
|
|
5
|
+
@click.version_option(version="0.2.0")
|
|
6
|
+
def main():
|
|
7
|
+
"""AeCode - DSL ultra-compacto para scraping, UI y APIs."""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
@main.command()
|
|
11
|
+
@click.argument('archivo', type=click.Path(exists=True))
|
|
12
|
+
def scrape(archivo):
|
|
13
|
+
"""Ejecuta un archivo .ae de scraping (AeWeb)."""
|
|
14
|
+
from .web_engine import ae_get, ae_select, ae_attr
|
|
15
|
+
code = Path(archivo).read_text(encoding='utf-8')
|
|
16
|
+
click.echo(f"Ejecutando scraper: {archivo}")
|
|
17
|
+
click.echo(code)
|
|
18
|
+
click.echo("Scraper ejecutado.")
|
|
19
|
+
|
|
20
|
+
@main.command()
|
|
21
|
+
@click.argument('archivo', type=click.Path(exists=True))
|
|
22
|
+
@click.option('--output', '-o', default='output.html', help='Archivo HTML de salida')
|
|
23
|
+
def build(archivo, output):
|
|
24
|
+
"""Compila un archivo .ae de UI a HTML (AeUI)."""
|
|
25
|
+
from .ui_engine import build_and_open
|
|
26
|
+
code = Path(archivo).read_text(encoding='utf-8')
|
|
27
|
+
click.echo(f"Compilando UI: {archivo}")
|
|
28
|
+
build_and_open(code, output)
|
|
29
|
+
click.echo(f"Listo: {output}")
|
|
30
|
+
|
|
31
|
+
@main.command()
|
|
32
|
+
@click.argument('archivo', type=click.Path(exists=True))
|
|
33
|
+
def flow(archivo):
|
|
34
|
+
"""Ejecuta un archivo .ae de flujo completo (Scrape + UI)."""
|
|
35
|
+
from .flow_engine import run_flow
|
|
36
|
+
code = Path(archivo).read_text(encoding='utf-8')
|
|
37
|
+
click.echo(f"Ejecutando flow: {archivo}")
|
|
38
|
+
run_flow(code)
|
|
39
|
+
click.echo("Flow completado.")
|
|
40
|
+
|
|
41
|
+
@main.command()
|
|
42
|
+
@click.argument('archivo', type=click.Path(exists=True))
|
|
43
|
+
def api(archivo):
|
|
44
|
+
"""Ejecuta un archivo .ae de consumo de APIs (AeAPI)."""
|
|
45
|
+
from .api_engine import run_api
|
|
46
|
+
code = Path(archivo).read_text(encoding='utf-8')
|
|
47
|
+
click.echo(f"Ejecutando API: {archivo}")
|
|
48
|
+
run_api(code)
|
|
49
|
+
click.echo("API completado.")
|
|
50
|
+
|
|
51
|
+
@main.command()
|
|
52
|
+
def doctor():
|
|
53
|
+
"""Verifica dependencias instaladas."""
|
|
54
|
+
deps = ['requests', 'bs4', 'lark', 'click']
|
|
55
|
+
nombres = ['requests', 'beautifulsoup4', 'lark', 'click']
|
|
56
|
+
for dep, nombre in zip(deps, nombres):
|
|
57
|
+
try:
|
|
58
|
+
__import__(dep)
|
|
59
|
+
click.echo(f" OK: {nombre}")
|
|
60
|
+
except ImportError:
|
|
61
|
+
click.echo(f" FALTA: {nombre} (pip install {nombre})")
|
aecode/flow_engine.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import webbrowser
|
|
2
|
+
import re
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from .web_engine import scrape
|
|
5
|
+
from .ui_engine import build_ui
|
|
6
|
+
|
|
7
|
+
def flow_scrape(url, item_selector, fields):
|
|
8
|
+
print(f"[AeFlow] Scraping {url}...")
|
|
9
|
+
results = scrape(url, item_selector, fields)
|
|
10
|
+
print(f"[AeFlow] {len(results)} items extraidos con éxito")
|
|
11
|
+
return results
|
|
12
|
+
|
|
13
|
+
def flow_grid(data, cols, fields):
|
|
14
|
+
cols = int(cols)
|
|
15
|
+
if not data:
|
|
16
|
+
print("⚠️ [AeFlow] Advertencia: flow_grid recibió una lista vacía de datos.")
|
|
17
|
+
return '''<div class="max-w-7xl mx-auto px-6 py-12 text-center text-gray-400">
|
|
18
|
+
<p class="text-xl font-semibold mb-2">No se encontraron datos</p>
|
|
19
|
+
<p class="text-sm">Verifica la URL o los selectores CSS del paso SCRAPE.</p>
|
|
20
|
+
</div>'''
|
|
21
|
+
|
|
22
|
+
cards_html = []
|
|
23
|
+
for idx, item in enumerate(data, 1):
|
|
24
|
+
title_val = item.get(fields[0], item.get('title', f"Item {idx}")) if fields else item.get('title', f"Item {idx}")
|
|
25
|
+
price_val = item.get(fields[1], item.get('price', "")) if fields and len(fields) > 1 else item.get('price', "")
|
|
26
|
+
|
|
27
|
+
card = f''' <div class="bg-gray-900 border border-gray-800 hover:border-amber-500/50 rounded-2xl p-5 shadow-xl transition-all duration-300 hover:-translate-y-2 flex flex-col justify-between group">
|
|
28
|
+
<div>
|
|
29
|
+
<div class="h-40 bg-gradient-to-tr from-amber-950/40 to-gray-800 rounded-xl mb-4 flex items-center justify-center text-amber-400 group-hover:scale-[1.02] transition-transform">
|
|
30
|
+
<svg class="w-12 h-12 opacity-80" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path></svg>
|
|
31
|
+
</div>
|
|
32
|
+
<span class="text-xs font-semibold px-2.5 py-1 bg-amber-500/10 text-amber-400 rounded-full border border-amber-500/20 mb-2 inline-block">Item #{idx}</span>
|
|
33
|
+
<h3 class="text-lg font-bold text-white mb-2 line-clamp-2 leading-snug group-hover:text-amber-300 transition-colors" title="{title_val}">{title_val}</h3>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="pt-3 border-t border-gray-800 flex items-center justify-between mt-3">
|
|
36
|
+
<span class="text-2xl font-black text-amber-400">{price_val}</span>
|
|
37
|
+
<button onclick="alert('Seleccionado: {title_val}')" class="px-4 py-2 bg-gradient-to-r from-amber-500 to-orange-600 hover:from-amber-400 hover:to-orange-500 text-gray-950 font-bold text-sm rounded-xl shadow-md transition transform active:scale-95 cursor-pointer">Ver</button>
|
|
38
|
+
</div>
|
|
39
|
+
</div>'''
|
|
40
|
+
cards_html.append(card)
|
|
41
|
+
|
|
42
|
+
view_html = f'''<div class="max-w-7xl mx-auto px-6 py-8">
|
|
43
|
+
<div class="mb-8 pb-6 border-b border-gray-800 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
44
|
+
<div>
|
|
45
|
+
<h2 class="text-3xl font-extrabold text-white">Catálogo de Libros en Vivo</h2>
|
|
46
|
+
<p class="text-gray-400 text-sm mt-1">{len(data)} items extraídos con éxito | AeFlow Engine</p>
|
|
47
|
+
</div>
|
|
48
|
+
<div>
|
|
49
|
+
<span class="px-4 py-2 bg-gray-900 border border-gray-800 text-amber-400 text-sm font-bold rounded-xl shadow-sm">
|
|
50
|
+
Total: {len(data)} items
|
|
51
|
+
</span>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-{cols} gap-6">
|
|
55
|
+
{''.join(cards_html)}
|
|
56
|
+
</div>
|
|
57
|
+
</div>'''
|
|
58
|
+
return view_html
|
|
59
|
+
|
|
60
|
+
def flow_render(view_content, output="aeflow_result.html"):
|
|
61
|
+
if not view_content:
|
|
62
|
+
view_content = '<div class="p-12 text-center text-red-400 font-bold">Error: No hay contenido para renderizar en VIEW.</div>'
|
|
63
|
+
|
|
64
|
+
full_html = f"""<!DOCTYPE html>
|
|
65
|
+
<html lang="es">
|
|
66
|
+
<head>
|
|
67
|
+
<meta charset="UTF-8">
|
|
68
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
69
|
+
<title>AeFlow Dashboard</title>
|
|
70
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
71
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
72
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
73
|
+
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;800;900&display=swap" rel="stylesheet">
|
|
74
|
+
<style>body {{ font-family: 'Plus Jakarta Sans', sans-serif; }}</style>
|
|
75
|
+
</head>
|
|
76
|
+
<body class="bg-gray-950 text-gray-100 min-h-screen">
|
|
77
|
+
<nav class="bg-gray-900 border-b border-gray-800 px-8 py-4 flex items-center justify-between sticky top-0 z-50 backdrop-blur bg-gray-900/90 shadow-md">
|
|
78
|
+
<div class="flex items-center gap-3">
|
|
79
|
+
<div class="w-8 h-8 rounded-lg bg-gradient-to-tr from-amber-500 to-orange-600 flex items-center justify-center font-black text-gray-950 text-lg">⚡</div>
|
|
80
|
+
<span class="font-extrabold text-xl text-white">Ae<span class="text-amber-400">Flow</span></span>
|
|
81
|
+
</div>
|
|
82
|
+
<span class="px-3 py-1 rounded-full bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 text-xs font-semibold">● Live Scraped</span>
|
|
83
|
+
</nav>
|
|
84
|
+
{view_content}
|
|
85
|
+
<footer class="text-center py-8 text-gray-600 border-t border-gray-900 text-sm mt-12">Generado automáticamente por AeCode Engine</footer>
|
|
86
|
+
</body>
|
|
87
|
+
</html>"""
|
|
88
|
+
out_path = Path(output)
|
|
89
|
+
out_path.write_text(full_html, encoding='utf-8')
|
|
90
|
+
print(f"Generado: {out_path.resolve()}")
|
|
91
|
+
try:
|
|
92
|
+
webbrowser.open(out_path.as_uri())
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
return full_html
|
|
96
|
+
|
|
97
|
+
def run_flow(code):
|
|
98
|
+
lines = [l.strip() for l in code.strip().split('\n') if l.strip() and not l.strip().startswith('//') and not l.strip().startswith('#')]
|
|
99
|
+
scope = {}
|
|
100
|
+
|
|
101
|
+
for line in lines:
|
|
102
|
+
# 1. SCRAPE
|
|
103
|
+
m_scrape = re.match(
|
|
104
|
+
r'(\w+)\s*=\s*SCRAPE\s*\(\s*["\']([^"\']+)["\']\s*,\s*["\']([^"\']+)["\']\s*,\s*[\[\{](.+)[\]\}]\s*\)',
|
|
105
|
+
line
|
|
106
|
+
)
|
|
107
|
+
if m_scrape:
|
|
108
|
+
var_name = m_scrape.group(1)
|
|
109
|
+
url = m_scrape.group(2)
|
|
110
|
+
selector = m_scrape.group(3)
|
|
111
|
+
fields_str = m_scrape.group(4)
|
|
112
|
+
fields = {}
|
|
113
|
+
for fm in re.finditer(r'(\w+)\s*:\s*["\']([^"\']+)["\']\s*@\s*["\']([^"\']+)["\']', fields_str):
|
|
114
|
+
fields[fm.group(1)] = (fm.group(2), fm.group(3))
|
|
115
|
+
scope[var_name] = flow_scrape(url, selector, fields)
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
# 2. GRID > CARD
|
|
119
|
+
m_grid = re.match(
|
|
120
|
+
r'(\w+)\s*=\s*GRID\s+(\d+)\s*>\s*CARD\s*\(([^)]+)\)\s+(?:FROM|from)\s+(\w+)',
|
|
121
|
+
line
|
|
122
|
+
)
|
|
123
|
+
if m_grid:
|
|
124
|
+
view_var = m_grid.group(1)
|
|
125
|
+
cols = m_grid.group(2)
|
|
126
|
+
fields_list = [f.strip() for f in m_grid.group(3).split(',')]
|
|
127
|
+
from_var = m_grid.group(4)
|
|
128
|
+
data_source = scope.get(from_var, [])
|
|
129
|
+
scope[view_var] = flow_grid(data_source, cols, fields_list)
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
# 3. RENDER
|
|
133
|
+
m_render = re.match(r'RENDER\s+(\w+)', line)
|
|
134
|
+
if m_render:
|
|
135
|
+
render_var = m_render.group(1)
|
|
136
|
+
view_html = scope.get(render_var, "")
|
|
137
|
+
flow_render(view_html)
|
|
138
|
+
continue
|