fresco 0.0.1
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.
- checksums.yaml +7 -0
- data/exe/fresco +3 -0
- data/lib/fresco/application.rb +12 -0
- data/lib/fresco/cli/build.rb +682 -0
- data/lib/fresco/cli/dev.rb +17 -0
- data/lib/fresco/cli/dev_loop.rb +815 -0
- data/lib/fresco/cli/new.rb +120 -0
- data/lib/fresco/cli/release.rb +76 -0
- data/lib/fresco/cli.rb +56 -0
- data/lib/fresco/database_config.rb +34 -0
- data/lib/fresco/generators/app/Gemfile.tt +18 -0
- data/lib/fresco/generators/app/README.md.tt +32 -0
- data/lib/fresco/generators/app/app/action.rb.tt +20 -0
- data/lib/fresco/generators/app/app/actions/root_path.rb.tt +5 -0
- data/lib/fresco/generators/app/app/views/layouts/application.html.erb +29 -0
- data/lib/fresco/generators/app/app/views/root_path.html.erb +8 -0
- data/lib/fresco/generators/app/app.rb.tt +15 -0
- data/lib/fresco/generators/app/bin/build +2 -0
- data/lib/fresco/generators/app/bin/dev +2 -0
- data/lib/fresco/generators/app/bin/release +2 -0
- data/lib/fresco/generators/app/config/app.rb.tt +26 -0
- data/lib/fresco/generators/app/config/database.rb +17 -0
- data/lib/fresco/generators/app/config/routes.rb +11 -0
- data/lib/fresco/generators/app/db/schema.rb +14 -0
- data/lib/fresco/generators/app/public/404.html +87 -0
- data/lib/fresco/generators/app/public/500.html +84 -0
- data/lib/fresco/migration_builder.rb +55 -0
- data/lib/fresco/model_builder.rb +54 -0
- data/lib/fresco/paths.rb +20 -0
- data/lib/fresco/router.rb +67 -0
- data/lib/fresco/runtime/boot.rb +34 -0
- data/lib/fresco/runtime/db_postgres.rb +403 -0
- data/lib/fresco/runtime/db_sqlite.rb +495 -0
- data/lib/fresco/runtime/http.c +456 -0
- data/lib/fresco/runtime/postgres.c +339 -0
- data/lib/fresco/runtime/runtime.rb +1810 -0
- data/lib/fresco/runtime/sqlite.c +220 -0
- data/lib/fresco/runtime/welcome.rb +152 -0
- data/lib/fresco/schema_builder.rb +71 -0
- data/lib/fresco/templates/dispatch.rb.erb +32 -0
- data/lib/fresco/templates/layout_dispatch.rb.erb +16 -0
- data/lib/fresco/templates/manifest.rb.erb +5 -0
- data/lib/fresco/templates/migrations.rb.erb +152 -0
- data/lib/fresco/templates/model.rb.erb +223 -0
- data/lib/fresco/templates/view.rb.erb +5 -0
- data/lib/fresco/version.rb +3 -0
- data/lib/fresco.rb +61 -0
- metadata +115 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Fresco SQLite shim. Wraps libsqlite3 behind a stable str/int FFI
|
|
3
|
+
* surface so Spinel-AOT'd apps can talk to a database without going
|
|
4
|
+
* through CRuby's `sqlite3` gem (which ships native code via the MRI
|
|
5
|
+
* ABI — not loadable by Spinel).
|
|
6
|
+
*
|
|
7
|
+
* Pattern mirrors lib/http.c: spinel-friendly types only (:int, :str),
|
|
8
|
+
* static buffers for return strings, slot tables for opaque handles
|
|
9
|
+
* so the Ruby side never sees a raw pointer.
|
|
10
|
+
*
|
|
11
|
+
* Lifted from tep's lib/tep/tep_sqlite.c. Two intentional divergences
|
|
12
|
+
* from the original:
|
|
13
|
+
*
|
|
14
|
+
* 1. Multi-cursor. Tep keeps a single global `sqlite3_stmt *`, which
|
|
15
|
+
* means one in-flight query per process — fine for hand-written
|
|
16
|
+
* route handlers, too tight once the codegen tier (M3+) starts
|
|
17
|
+
* stacking finders. We track up to FRESCO_SQLITE_MAX_STMTS
|
|
18
|
+
* cursors in a slot table and return an int cursor id from
|
|
19
|
+
* fresco_sqlite_prepare; subsequent bind/step/col/finalize/reset
|
|
20
|
+
* calls take the cursor id as their first argument.
|
|
21
|
+
*
|
|
22
|
+
* 2. Naming. `fresco_sqlite_*` rather than `tep_sqlite_*`.
|
|
23
|
+
*
|
|
24
|
+
* Errors
|
|
25
|
+
* ------
|
|
26
|
+
* Most functions return 0 on success and -1 on error. We don't
|
|
27
|
+
* surface sqlite3_errmsg through the FFI in v1 — the :str return
|
|
28
|
+
* lifetime would have to be threaded through another rotating buffer
|
|
29
|
+
* and callers haven't asked for it yet.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
#include <stdlib.h>
|
|
33
|
+
#include <string.h>
|
|
34
|
+
#include <sqlite3.h>
|
|
35
|
+
|
|
36
|
+
#define FRESCO_SQLITE_MAX_HANDLES 16
|
|
37
|
+
#define FRESCO_SQLITE_MAX_STMTS 64
|
|
38
|
+
#define FRESCO_SQLITE_COL_BUFSIZE 65536
|
|
39
|
+
#define FRESCO_SQLITE_COL_BUF_SLOTS 16
|
|
40
|
+
|
|
41
|
+
static sqlite3 *fresco_sqlite_handles[FRESCO_SQLITE_MAX_HANDLES] = {0};
|
|
42
|
+
static sqlite3_stmt *fresco_sqlite_stmts[FRESCO_SQLITE_MAX_STMTS] = {0};
|
|
43
|
+
|
|
44
|
+
/* Rotating return buffers for col_str. Spinel's `:str` return type
|
|
45
|
+
* wants a pointer that stays valid until "the caller is done with
|
|
46
|
+
* it", but in practice callers stash multiple col_str results into
|
|
47
|
+
* variables / hashes / array entries before the buffer would
|
|
48
|
+
* otherwise rotate. A single static buf would alias all those
|
|
49
|
+
* entries to whatever the most recent call wrote.
|
|
50
|
+
*
|
|
51
|
+
* We rotate across SLOTS buffers; each call lands in the next
|
|
52
|
+
* slot. With 16 slots a typical handler doing
|
|
53
|
+
*
|
|
54
|
+
* a = first_str(...); b = first_str(...); c = first_str(...)
|
|
55
|
+
*
|
|
56
|
+
* sees three independent strings. Callers that hold > SLOTS live
|
|
57
|
+
* col_str references concurrently must copy each result before the
|
|
58
|
+
* next call. */
|
|
59
|
+
static char fresco_sqlite_col_buf[FRESCO_SQLITE_COL_BUF_SLOTS][FRESCO_SQLITE_COL_BUFSIZE];
|
|
60
|
+
static int fresco_sqlite_col_slot = 0;
|
|
61
|
+
|
|
62
|
+
/* Open a database. Returns a 1-indexed handle on success (0 reserved
|
|
63
|
+
* for "uninitialised"); -1 if the slot table is full or sqlite3_open
|
|
64
|
+
* rejects the path. `:memory:` works as a path for anonymous in-mem
|
|
65
|
+
* databases. Closing frees the slot for re-use. */
|
|
66
|
+
int fresco_sqlite_open(const char *path) {
|
|
67
|
+
int i;
|
|
68
|
+
for (i = 0; i < FRESCO_SQLITE_MAX_HANDLES; i++) {
|
|
69
|
+
if (fresco_sqlite_handles[i] == NULL) {
|
|
70
|
+
sqlite3 *db = NULL;
|
|
71
|
+
if (sqlite3_open(path, &db) != SQLITE_OK) {
|
|
72
|
+
if (db) sqlite3_close(db);
|
|
73
|
+
return -1;
|
|
74
|
+
}
|
|
75
|
+
fresco_sqlite_handles[i] = db;
|
|
76
|
+
return i + 1;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return -1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
int fresco_sqlite_close(int h) {
|
|
83
|
+
int i;
|
|
84
|
+
if (h < 1 || h > FRESCO_SQLITE_MAX_HANDLES) return -1;
|
|
85
|
+
sqlite3 *db = fresco_sqlite_handles[h - 1];
|
|
86
|
+
if (db == NULL) return -1;
|
|
87
|
+
/* Finalize any cursor still attached to this handle to avoid
|
|
88
|
+
* sqlite3_close returning SQLITE_BUSY. */
|
|
89
|
+
for (i = 0; i < FRESCO_SQLITE_MAX_STMTS; i++) {
|
|
90
|
+
if (fresco_sqlite_stmts[i] != NULL &&
|
|
91
|
+
sqlite3_db_handle(fresco_sqlite_stmts[i]) == db) {
|
|
92
|
+
sqlite3_finalize(fresco_sqlite_stmts[i]);
|
|
93
|
+
fresco_sqlite_stmts[i] = NULL;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
sqlite3_close(db);
|
|
97
|
+
fresco_sqlite_handles[h - 1] = NULL;
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* No-bind statement runner. Use for DDL, BEGIN/COMMIT, and queries
|
|
102
|
+
* whose values are constants. For anything user-supplied always go
|
|
103
|
+
* through prepare + bind + step + finalize. */
|
|
104
|
+
int fresco_sqlite_exec(int h, const char *sql) {
|
|
105
|
+
if (h < 1 || h > FRESCO_SQLITE_MAX_HANDLES) return -1;
|
|
106
|
+
sqlite3 *db = fresco_sqlite_handles[h - 1];
|
|
107
|
+
if (db == NULL) return -1;
|
|
108
|
+
char *err = NULL;
|
|
109
|
+
int rc = sqlite3_exec(db, sql, NULL, NULL, &err);
|
|
110
|
+
if (err) sqlite3_free(err);
|
|
111
|
+
return rc == SQLITE_OK ? 0 : -1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* Prepare a parameterised statement on handle `h`. Returns a 1-indexed
|
|
115
|
+
* cursor id on success, -1 on failure or if the slot table is full.
|
|
116
|
+
* The cursor id is the first argument to subsequent bind/step/col/
|
|
117
|
+
* finalize/reset calls — callers can hold several cursors at once. */
|
|
118
|
+
int fresco_sqlite_prepare(int h, const char *sql) {
|
|
119
|
+
int i;
|
|
120
|
+
if (h < 1 || h > FRESCO_SQLITE_MAX_HANDLES) return -1;
|
|
121
|
+
sqlite3 *db = fresco_sqlite_handles[h - 1];
|
|
122
|
+
if (db == NULL) return -1;
|
|
123
|
+
for (i = 0; i < FRESCO_SQLITE_MAX_STMTS; i++) {
|
|
124
|
+
if (fresco_sqlite_stmts[i] == NULL) {
|
|
125
|
+
sqlite3_stmt *stmt = NULL;
|
|
126
|
+
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
127
|
+
return -1;
|
|
128
|
+
}
|
|
129
|
+
fresco_sqlite_stmts[i] = stmt;
|
|
130
|
+
return i + 1;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return -1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
int fresco_sqlite_bind_str(int cid, int idx, const char *value) {
|
|
137
|
+
if (cid < 1 || cid > FRESCO_SQLITE_MAX_STMTS) return -1;
|
|
138
|
+
sqlite3_stmt *stmt = fresco_sqlite_stmts[cid - 1];
|
|
139
|
+
if (stmt == NULL) return -1;
|
|
140
|
+
/* SQLITE_TRANSIENT — sqlite copies the string before returning,
|
|
141
|
+
* so the caller's pointer doesn't need to outlive the bind. */
|
|
142
|
+
return sqlite3_bind_text(stmt, idx, value, -1, SQLITE_TRANSIENT)
|
|
143
|
+
== SQLITE_OK ? 0 : -1;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
int fresco_sqlite_bind_int(int cid, int idx, int value) {
|
|
147
|
+
if (cid < 1 || cid > FRESCO_SQLITE_MAX_STMTS) return -1;
|
|
148
|
+
sqlite3_stmt *stmt = fresco_sqlite_stmts[cid - 1];
|
|
149
|
+
if (stmt == NULL) return -1;
|
|
150
|
+
return sqlite3_bind_int(stmt, idx, value) == SQLITE_OK ? 0 : -1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* 1 -> row available, 0 -> done (no more rows), -1 -> error or invalid
|
|
154
|
+
* cursor. Iteration pattern: `while step(cid) == 1; ...; end`. */
|
|
155
|
+
int fresco_sqlite_step(int cid) {
|
|
156
|
+
if (cid < 1 || cid > FRESCO_SQLITE_MAX_STMTS) return -1;
|
|
157
|
+
sqlite3_stmt *stmt = fresco_sqlite_stmts[cid - 1];
|
|
158
|
+
if (stmt == NULL) return -1;
|
|
159
|
+
int rc = sqlite3_step(stmt);
|
|
160
|
+
if (rc == SQLITE_ROW) return 1;
|
|
161
|
+
if (rc == SQLITE_DONE) return 0;
|
|
162
|
+
return -1;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const char *fresco_sqlite_col_str(int cid, int idx) {
|
|
166
|
+
if (cid < 1 || cid > FRESCO_SQLITE_MAX_STMTS) return "";
|
|
167
|
+
sqlite3_stmt *stmt = fresco_sqlite_stmts[cid - 1];
|
|
168
|
+
if (stmt == NULL) return "";
|
|
169
|
+
const unsigned char *t = sqlite3_column_text(stmt, idx);
|
|
170
|
+
if (!t) return "";
|
|
171
|
+
size_t n = strlen((const char *)t);
|
|
172
|
+
if (n >= FRESCO_SQLITE_COL_BUFSIZE) n = FRESCO_SQLITE_COL_BUFSIZE - 1;
|
|
173
|
+
char *buf = fresco_sqlite_col_buf[fresco_sqlite_col_slot];
|
|
174
|
+
fresco_sqlite_col_slot = (fresco_sqlite_col_slot + 1) % FRESCO_SQLITE_COL_BUF_SLOTS;
|
|
175
|
+
memcpy(buf, t, n);
|
|
176
|
+
buf[n] = '\0';
|
|
177
|
+
return buf;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
int fresco_sqlite_col_int(int cid, int idx) {
|
|
181
|
+
if (cid < 1 || cid > FRESCO_SQLITE_MAX_STMTS) return 0;
|
|
182
|
+
sqlite3_stmt *stmt = fresco_sqlite_stmts[cid - 1];
|
|
183
|
+
if (stmt == NULL) return 0;
|
|
184
|
+
return sqlite3_column_int(stmt, idx);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
int fresco_sqlite_col_count(int cid) {
|
|
188
|
+
if (cid < 1 || cid > FRESCO_SQLITE_MAX_STMTS) return 0;
|
|
189
|
+
sqlite3_stmt *stmt = fresco_sqlite_stmts[cid - 1];
|
|
190
|
+
if (stmt == NULL) return 0;
|
|
191
|
+
return sqlite3_column_count(stmt);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
int fresco_sqlite_finalize(int cid) {
|
|
195
|
+
if (cid < 1 || cid > FRESCO_SQLITE_MAX_STMTS) return 0;
|
|
196
|
+
sqlite3_stmt *stmt = fresco_sqlite_stmts[cid - 1];
|
|
197
|
+
if (stmt == NULL) return 0;
|
|
198
|
+
sqlite3_finalize(stmt);
|
|
199
|
+
fresco_sqlite_stmts[cid - 1] = NULL;
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* Reset a statement so it can be re-stepped (e.g. inside a loop
|
|
204
|
+
* where bound params change between iterations). Bindings survive
|
|
205
|
+
* the reset; sqlite3_clear_bindings is intentionally NOT called so
|
|
206
|
+
* callers can re-step with the same values cheaply. Use a fresh
|
|
207
|
+
* prepare if you need cleared bindings. */
|
|
208
|
+
int fresco_sqlite_reset(int cid) {
|
|
209
|
+
if (cid < 1 || cid > FRESCO_SQLITE_MAX_STMTS) return -1;
|
|
210
|
+
sqlite3_stmt *stmt = fresco_sqlite_stmts[cid - 1];
|
|
211
|
+
if (stmt == NULL) return -1;
|
|
212
|
+
return sqlite3_reset(stmt) == SQLITE_OK ? 0 : -1;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
int fresco_sqlite_last_insert_rowid(int h) {
|
|
216
|
+
if (h < 1 || h > FRESCO_SQLITE_MAX_HANDLES) return -1;
|
|
217
|
+
sqlite3 *db = fresco_sqlite_handles[h - 1];
|
|
218
|
+
if (db == NULL) return -1;
|
|
219
|
+
return (int)sqlite3_last_insert_rowid(db);
|
|
220
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Fresco: default welcome action. Auto-generated into
|
|
2
|
+
# generated/welcome.rb by `fresco build` when config/routes.rb doesn't
|
|
3
|
+
# define a `root to: ...` route. Source of truth is
|
|
4
|
+
# lib/fresco/runtime/welcome.rb in the fresco gem — `fresco build`
|
|
5
|
+
# copies this file verbatim (same handling as runtime.rb / boot.rb).
|
|
6
|
+
#
|
|
7
|
+
# Inherits Fresco::Action directly (not App::Action) so the welcome
|
|
8
|
+
# page doesn't depend on the user defining a :application layout. The
|
|
9
|
+
# body is built inline via Response.html, bypassing #render and
|
|
10
|
+
# dispatch_layout entirely — no view template, no layout, no coupling
|
|
11
|
+
# with user code.
|
|
12
|
+
#
|
|
13
|
+
# Style matches public/404.html / public/500.html: IBM Plex Mono,
|
|
14
|
+
# 40rem column, CSS-variable light/dark theming with the same toggle
|
|
15
|
+
# button. Pre-paint script runs from localStorage so the page doesn't
|
|
16
|
+
# flash light before JS executes. Stop seeing this page by adding
|
|
17
|
+
# `root to: SomeAction` to config/routes.rb.
|
|
18
|
+
|
|
19
|
+
module Fresco
|
|
20
|
+
class Welcome < Fresco::Action
|
|
21
|
+
def call(req = Request.new("", "", ""))
|
|
22
|
+
Response.html(200, welcome_body)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Manual string concat (matches build_headers / Json.encode_string
|
|
26
|
+
# in runtime.rb) keeps each intermediate narrowly typed String for
|
|
27
|
+
# Spinel — heredocs and `<<~`-style literals don't appear elsewhere
|
|
28
|
+
# in the Spinel-compiled surface, so we stick to the established
|
|
29
|
+
# `out += ...` pattern.
|
|
30
|
+
def welcome_body
|
|
31
|
+
out = "<!doctype html>"
|
|
32
|
+
out += "<html>"
|
|
33
|
+
out += "<head>"
|
|
34
|
+
out += "<meta charset=\"utf-8\">"
|
|
35
|
+
out += "<title>Welcome to Fresco</title>"
|
|
36
|
+
out += "<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">"
|
|
37
|
+
out += "<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>"
|
|
38
|
+
out += "<link href=\"https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;700&display=swap\" rel=\"stylesheet\">"
|
|
39
|
+
out += "<script>"
|
|
40
|
+
out += "(function () {"
|
|
41
|
+
out += " var t = localStorage.getItem(\"theme\");"
|
|
42
|
+
out += " if (!t) {"
|
|
43
|
+
out += " t = (window.matchMedia && window.matchMedia(\"(prefers-color-scheme: dark)\").matches) ? \"dark\" : \"light\";"
|
|
44
|
+
out += " localStorage.setItem(\"theme\", t);"
|
|
45
|
+
out += " }"
|
|
46
|
+
out += " document.documentElement.setAttribute(\"data-theme\", t);"
|
|
47
|
+
out += "})();"
|
|
48
|
+
out += "</script>"
|
|
49
|
+
out += "<style>"
|
|
50
|
+
out += ":root { --bg: #f5f5f4; --fg: #1c1917; --muted: #78716c; --border: #e7e5e4; --code-bg: #e7e5e4; --accent: #0066cc; --status: #78716c; }"
|
|
51
|
+
out += ":root[data-theme=\"dark\"] { --bg: #1c1917; --fg: #e7e5e4; --muted: #a8a29e; --border: #292524; --code-bg: #292524; --accent: #6ab0ff; --status: #a8a29e; }"
|
|
52
|
+
out += "body { font-family: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace; max-width: 40rem; margin: 4rem auto; padding: 0 1.5rem; background: var(--bg); color: var(--fg); transition: background 0.15s, color 0.15s; }"
|
|
53
|
+
out += "h1 { font-weight: 700; font-size: 2.2rem; margin: 0 0 0.5rem; line-height: 1.2; }"
|
|
54
|
+
out += ".status { color: var(--status); font-size: 0.9rem; letter-spacing: 0.05em; text-transform: uppercase; }"
|
|
55
|
+
out += ".lede { color: var(--muted); margin: 0 0 1.75rem; }"
|
|
56
|
+
out += "p { line-height: 1.5; }"
|
|
57
|
+
out += "code { background: var(--code-bg); padding: 0.1rem 0.3rem; border-radius: 3px; }"
|
|
58
|
+
out += "a { color: var(--accent); }"
|
|
59
|
+
out += "a:hover { text-decoration: underline; }"
|
|
60
|
+
out += "hr { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }"
|
|
61
|
+
out += "footer { color: var(--muted); font-size: 0.85rem; }"
|
|
62
|
+
out += "#theme-toggle { position: fixed; top: 1rem; right: 1rem; background: transparent; border: 1px solid var(--border); color: var(--fg); border-radius: 6px; padding: 0.4rem; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }"
|
|
63
|
+
out += "#theme-toggle:hover { background: var(--code-bg); }"
|
|
64
|
+
out += "#theme-toggle svg { width: 18px; height: 18px; display: none; }"
|
|
65
|
+
out += ":root[data-theme=\"light\"] #theme-toggle .icon-moon { display: inline-block; }"
|
|
66
|
+
out += ":root[data-theme=\"dark\"] #theme-toggle .icon-sun { display: inline-block; }"
|
|
67
|
+
# Trans pride coloring on the "Fresco" wordmark — 6 letters laid
|
|
68
|
+
# out as a mirror of the 5-stripe flag (blue / pink / white /
|
|
69
|
+
# pink / blue). The two middle letters take --fg as a stand-in
|
|
70
|
+
# for "white" so they stay readable on both stone backgrounds
|
|
71
|
+
# (renders as dark stone in light mode, near-white in dark mode).
|
|
72
|
+
# Light mode uses deeper blue/pink variants so they don't wash
|
|
73
|
+
# out against #f5f5f4; dark mode uses the canonical pastels which
|
|
74
|
+
# pop on #1c1917.
|
|
75
|
+
out += ".pride { font-weight: 700; letter-spacing: 0.04em; margin-left: 0.1em; }"
|
|
76
|
+
out += ".pride span { display: inline-block; }"
|
|
77
|
+
out += ".pride .p1 { color: #2dbfe0; }"
|
|
78
|
+
out += ".pride .p2 { color: #ed7b95; }"
|
|
79
|
+
out += ".pride .p3 { color: var(--fg); }"
|
|
80
|
+
out += ".pride .p4 { color: var(--fg); }"
|
|
81
|
+
out += ".pride .p5 { color: #ed7b95; }"
|
|
82
|
+
out += ".pride .p6 { color: #2dbfe0; }"
|
|
83
|
+
out += ":root[data-theme=\"dark\"] .pride .p1 { color: #5bcefa; }"
|
|
84
|
+
out += ":root[data-theme=\"dark\"] .pride .p2 { color: #f5a9b8; }"
|
|
85
|
+
out += ":root[data-theme=\"dark\"] .pride .p5 { color: #f5a9b8; }"
|
|
86
|
+
out += ":root[data-theme=\"dark\"] .pride .p6 { color: #5bcefa; }"
|
|
87
|
+
# Two CTA cards (Docs / GitHub). Whole card is the clickable <a>;
|
|
88
|
+
# the icon, title, and "→" sit stacked inside. Flex layout, equal
|
|
89
|
+
# width, collapses to a single column on narrow viewports so the
|
|
90
|
+
# cards don't squeeze on mobile.
|
|
91
|
+
out += ".cards { display: flex; gap: 0.875rem; margin: 0 0 1.75rem; }"
|
|
92
|
+
out += ".cta { flex: 1; display: flex; flex-direction: column; gap: 0.5rem; padding: 1.25rem; border: 2px solid var(--border); border-radius: 8px; background: transparent; color: var(--fg); text-decoration: none; transition: transform 0.15s, border-color 0.15s; }"
|
|
93
|
+
# Trans pride hover border: two-layer background trick. The inner
|
|
94
|
+
# solid layer is clipped to padding-box and covers the card body;
|
|
95
|
+
# the gradient layer fills border-box and only shows through where
|
|
96
|
+
# the (now-transparent) border sits. The gradient encodes the
|
|
97
|
+
# 5-stripe flag (blue / pink / white / pink / blue) doubled
|
|
98
|
+
# end-to-end so panning 200% bg-size from 0 to -100% loops
|
|
99
|
+
# seamlessly — the second half is identical to the first.
|
|
100
|
+
out += ".cta:hover { border-color: transparent; transform: translateY(-1px); background-image: linear-gradient(var(--code-bg), var(--code-bg)), linear-gradient(90deg, #5bcefa, #f5a9b8, #ffffff, #f5a9b8, #5bcefa, #f5a9b8, #ffffff, #f5a9b8, #5bcefa); background-size: auto, 200% 100%; background-position: 0 0, 0 0; background-origin: border-box; background-clip: padding-box, border-box; animation: prideShift 4s linear infinite; }"
|
|
101
|
+
out += "@keyframes prideShift { to { background-position: 0 0, -100% 0; } }"
|
|
102
|
+
out += ".cta svg { width: 28px; height: 28px; stroke: var(--accent); }"
|
|
103
|
+
out += ".cta-title { font-weight: 700; font-size: 1.05rem; }"
|
|
104
|
+
out += ".cta-sub { color: var(--muted); font-size: 0.88rem; }"
|
|
105
|
+
out += ".cta:hover .cta-sub { color: var(--accent); }"
|
|
106
|
+
out += "@media (max-width: 30rem) { .cards { flex-direction: column; } }"
|
|
107
|
+
# Respect the OS reduce-motion preference — keep the rainbow
|
|
108
|
+
# border, just stop panning it for folks who opted out.
|
|
109
|
+
out += "@media (prefers-reduced-motion: reduce) { .cta:hover { animation: none; } }"
|
|
110
|
+
out += "</style>"
|
|
111
|
+
out += "</head>"
|
|
112
|
+
out += "<body>"
|
|
113
|
+
out += "<button id=\"theme-toggle\" aria-label=\"Toggle color theme\" title=\"Toggle color theme\">"
|
|
114
|
+
out += "<svg class=\"icon-sun\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"4\"/><path d=\"M12 2v2\"/><path d=\"M12 20v2\"/><path d=\"m4.93 4.93 1.41 1.41\"/><path d=\"m17.66 17.66 1.41 1.41\"/><path d=\"M2 12h2\"/><path d=\"M20 12h2\"/><path d=\"m6.34 17.66-1.41 1.41\"/><path d=\"m19.07 4.93-1.41 1.41\"/></svg>"
|
|
115
|
+
out += "<svg class=\"icon-moon\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z\"/></svg>"
|
|
116
|
+
out += "</button>"
|
|
117
|
+
out += "<div class=\"status\">200 · Welcome</div>"
|
|
118
|
+
out += "<h1>You're on <span class=\"pride\"><span class=\"p1\">F</span><span class=\"p2\">r</span><span class=\"p3\">e</span><span class=\"p4\">s</span><span class=\"p5\">c</span><span class=\"p6\">o</span></span>.</h1>"
|
|
119
|
+
out += "<p class=\"lede\">Spinel-compiled Ruby. Single binary. No runtime.</p>"
|
|
120
|
+
# Lucide icons (MIT) inlined as SVG. book-open → Docs;
|
|
121
|
+
# github → repo. Update href="#" with real URLs when ready.
|
|
122
|
+
out += "<div class=\"cards\">"
|
|
123
|
+
out += "<a class=\"cta\" href=\"https://github.com/afomera/fresco\">"
|
|
124
|
+
out += "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 7v14\"/><path d=\"M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z\"/></svg>"
|
|
125
|
+
out += "<div class=\"cta-title\">Documentation</div>"
|
|
126
|
+
out += "<div class=\"cta-sub\">Get started →</div>"
|
|
127
|
+
out += "</a>"
|
|
128
|
+
out += "<a class=\"cta\" href=\"https://github.com/afomera/fresco\">"
|
|
129
|
+
out += "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4\"/><path d=\"M9 18c-4.51 2-5-2-7-2\"/></svg>"
|
|
130
|
+
out += "<div class=\"cta-title\">GitHub</div>"
|
|
131
|
+
out += "<div class=\"cta-sub\">View source →</div>"
|
|
132
|
+
out += "</a>"
|
|
133
|
+
out += "</div>"
|
|
134
|
+
out += "<p>Next step: add <code>root to: SomeAction</code> to <code>config/routes.rb</code>, then rebuild and reload.</p>"
|
|
135
|
+
out += "<hr>"
|
|
136
|
+
out += "<footer>Powered by Spinel + Fresco. Served from the fresco gem's <code>welcome</code> action.</footer>"
|
|
137
|
+
out += "<script>"
|
|
138
|
+
out += "(function () {"
|
|
139
|
+
out += " document.getElementById(\"theme-toggle\").addEventListener(\"click\", function () {"
|
|
140
|
+
out += " var cur = document.documentElement.getAttribute(\"data-theme\");"
|
|
141
|
+
out += " var nxt = cur === \"dark\" ? \"light\" : \"dark\";"
|
|
142
|
+
out += " document.documentElement.setAttribute(\"data-theme\", nxt);"
|
|
143
|
+
out += " localStorage.setItem(\"theme\", nxt);"
|
|
144
|
+
out += " });"
|
|
145
|
+
out += "})();"
|
|
146
|
+
out += "</script>"
|
|
147
|
+
out += "</body>"
|
|
148
|
+
out += "</html>"
|
|
149
|
+
out
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Build-time schema DSL. Captured by db/schema.rb's `Fresco.schema do
|
|
2
|
+
# ... end` block; `fresco build` reads `Fresco.schema_tables` back out
|
|
3
|
+
# to drive the model codegen and (M5+) the embedded migration runner.
|
|
4
|
+
# Runtime never loads db/schema.rb — generated model files are
|
|
5
|
+
# self-contained.
|
|
6
|
+
#
|
|
7
|
+
# Shape captured per table:
|
|
8
|
+
# { name: :users,
|
|
9
|
+
# columns: [
|
|
10
|
+
# { name: :id, type: :int, primary_key: true },
|
|
11
|
+
# { name: :email, type: :str, null: false, index: :unique },
|
|
12
|
+
# ...
|
|
13
|
+
# ] }
|
|
14
|
+
#
|
|
15
|
+
# Columns are an ordered Array of Hashes so iteration order matches
|
|
16
|
+
# the SELECT clause order, which matches the column index the model
|
|
17
|
+
# initializer expects.
|
|
18
|
+
module Fresco
|
|
19
|
+
@schema_tables = []
|
|
20
|
+
|
|
21
|
+
def self.schema_tables
|
|
22
|
+
@schema_tables
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.schema(&blk)
|
|
26
|
+
builder = Fresco::SchemaBuilder.new
|
|
27
|
+
builder.instance_eval(&blk)
|
|
28
|
+
@schema_tables = builder.tables
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class Fresco::SchemaBuilder
|
|
33
|
+
attr_reader :tables
|
|
34
|
+
|
|
35
|
+
def initialize
|
|
36
|
+
@tables = []
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def table(name, &blk)
|
|
40
|
+
tb = Fresco::TableBuilder.new(name)
|
|
41
|
+
tb.instance_eval(&blk) if blk
|
|
42
|
+
@tables << { name: name, columns: tb.columns, foreign_keys: tb.foreign_keys }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class Fresco::TableBuilder
|
|
47
|
+
attr_reader :columns, :foreign_keys
|
|
48
|
+
|
|
49
|
+
def initialize(name)
|
|
50
|
+
@name = name
|
|
51
|
+
@columns = []
|
|
52
|
+
@foreign_keys = []
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# `type:` is one of :int, :str, :bool. `opts` is open — common
|
|
56
|
+
# keys: primary_key (Bool), null (Bool), index (:unique or any
|
|
57
|
+
# truthy), default (literal or Proc — M5+).
|
|
58
|
+
def column(name, type, **opts)
|
|
59
|
+
@columns << { name: name, type: type }.merge(opts)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Foreign-key metadata for the generated migration runner (M5+) and
|
|
63
|
+
# for M6+ join-helper codegen (`post.user`, `user.posts`). M4 only
|
|
64
|
+
# captures the entry — no SQL is emitted from it yet, and no methods
|
|
65
|
+
# on the model class reflect it. Convention: the column itself is
|
|
66
|
+
# declared with `column :user_id, :int` separately; this entry
|
|
67
|
+
# records the FK relationship only.
|
|
68
|
+
def foreign_key(column_name, references:)
|
|
69
|
+
@foreign_keys << { column: column_name, references: references }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Fresco: dispatcher. Auto-generated by bin/build; do not edit.
|
|
2
|
+
#
|
|
3
|
+
# Per-route filter chain is inlined here rather than going through
|
|
4
|
+
# Fresco::Action#handle. Spinel-shape constraint:
|
|
5
|
+
# `<klass>.new.handle(req)` lowers to the generic
|
|
6
|
+
# sp_Fresco_Action_handle, whose internal call(req) is a static
|
|
7
|
+
# dispatch to the base 500 fallback — the subclass's #call never
|
|
8
|
+
# runs. By keeping every method call on `_action` here at the
|
|
9
|
+
# concrete subclass type (sp_<Klass>_*), Spinel statically dispatches
|
|
10
|
+
# each one. Filter hooks (before_action / after_action / halt!) are
|
|
11
|
+
# inherited from Fresco::Action; subclasses override only the ones
|
|
12
|
+
# they need.
|
|
13
|
+
|
|
14
|
+
def dispatch_request(method, path, req)
|
|
15
|
+
<% routes.each_with_index do |r, i| -%>
|
|
16
|
+
<% if i > 0 -%>
|
|
17
|
+
|
|
18
|
+
<% end -%>
|
|
19
|
+
<% if r[:captures].empty? -%>
|
|
20
|
+
if method == <%= r[:verb].inspect %> && path == <%= r[:pattern].inspect %>
|
|
21
|
+
<% else -%>
|
|
22
|
+
if method == <%= r[:verb].inspect %> && path =~ /<%= r[:regex_src] %>/
|
|
23
|
+
<% r[:captures].each_with_index do |c, j| -%>
|
|
24
|
+
req.params.set_sym(:<%= c %>, $<%= j + 1 %> || "")
|
|
25
|
+
<% end -%>
|
|
26
|
+
<% end -%>
|
|
27
|
+
return <%= r[:klass] %>.new.handle(req)
|
|
28
|
+
end
|
|
29
|
+
<% end -%>
|
|
30
|
+
|
|
31
|
+
error_response(404, "Not Found")
|
|
32
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Fresco: layout dispatcher. Auto-generated by bin/build; do not edit.
|
|
2
|
+
#
|
|
3
|
+
# Maps the layout symbol declared on an action (via `layout :foo`) to
|
|
4
|
+
# the matching `render_layouts_<name>(flash: flash, content: body)` call.
|
|
5
|
+
# Symbol dispatch via static if/elsif so Spinel can lower each branch
|
|
6
|
+
# concretely — no send/public_send. Unknown / :none returns the body
|
|
7
|
+
# unchanged (flash is dropped since there's no layout to consume it).
|
|
8
|
+
|
|
9
|
+
def dispatch_layout(name, body, flash)
|
|
10
|
+
<% layout_names.each do |layout_name| -%>
|
|
11
|
+
if name == :<%= layout_name %>
|
|
12
|
+
return render_layouts_<%= layout_name %>(flash: flash, content: body)
|
|
13
|
+
end
|
|
14
|
+
<% end -%>
|
|
15
|
+
body
|
|
16
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Fresco migrations runner. Auto-generated by bin/build from
|
|
2
|
+
# db/migrations/*.rb. Do not edit.
|
|
3
|
+
#
|
|
4
|
+
# Each migration's up + down SQL is embedded as a flat method
|
|
5
|
+
# returning Array<String>. Spinel can pin those arrays at codegen
|
|
6
|
+
# time; the runner just iterates and execs them.
|
|
7
|
+
#
|
|
8
|
+
# Schema-version tracking lives in the `schema_migrations` table
|
|
9
|
+
# (one row per applied migration, keyed by name). migrate! runs
|
|
10
|
+
# pending up migrations in declaration order; rollback! reverses
|
|
11
|
+
# the most recently applied one. Both commands print one line per
|
|
12
|
+
# migration to stdout so the operator can see progress when running
|
|
13
|
+
# `./app db:migrate` over an SSH session.
|
|
14
|
+
|
|
15
|
+
module Fresco
|
|
16
|
+
module DbMigrations
|
|
17
|
+
# Applied-migrations sentinel table. Built lazily on the first
|
|
18
|
+
# migrate! / rollback! call so a fresh DB doesn't require manual
|
|
19
|
+
# setup. Idempotent — `CREATE TABLE IF NOT EXISTS` is portable
|
|
20
|
+
# enough across SQLite and Postgres.
|
|
21
|
+
def self.ensure_schema_table!
|
|
22
|
+
Fresco::Db.exec("CREATE TABLE IF NOT EXISTS schema_migrations (name TEXT PRIMARY KEY)")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Has `name` been applied already? Used to skip migrations that
|
|
26
|
+
# already ran. We read via a COUNT(*) rather than a SELECT-then-
|
|
27
|
+
# step pattern to keep the cursor lifecycle short.
|
|
28
|
+
def self.applied?(name = "")
|
|
29
|
+
cid = Fresco::Db.prepare("<%= applied_sql %>")
|
|
30
|
+
return false if cid < 0
|
|
31
|
+
Fresco::Db.bind_str(cid, 1, name)
|
|
32
|
+
n = 0
|
|
33
|
+
if Fresco::Db.step(cid) == 1
|
|
34
|
+
n = Fresco::Db.col_int(cid, 0)
|
|
35
|
+
end
|
|
36
|
+
Fresco::Db.finalize(cid)
|
|
37
|
+
n > 0
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.mark_applied!(name = "")
|
|
41
|
+
cid = Fresco::Db.prepare("<%= insert_applied_sql %>")
|
|
42
|
+
return if cid < 0
|
|
43
|
+
Fresco::Db.bind_str(cid, 1, name)
|
|
44
|
+
Fresco::Db.step(cid)
|
|
45
|
+
Fresco::Db.finalize(cid)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.unmark!(name = "")
|
|
49
|
+
cid = Fresco::Db.prepare("<%= delete_applied_sql %>")
|
|
50
|
+
return if cid < 0
|
|
51
|
+
Fresco::Db.bind_str(cid, 1, name)
|
|
52
|
+
Fresco::Db.step(cid)
|
|
53
|
+
Fresco::Db.finalize(cid)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Declaration order. Migrations are filename-sorted at build
|
|
57
|
+
# time so "0001..." comes before "0002...". Earlier entries
|
|
58
|
+
# also need to run first because later migrations may reference
|
|
59
|
+
# tables earlier ones created.
|
|
60
|
+
def self.names
|
|
61
|
+
[<%= migrations.map { |m| m[:name].inspect }.join(", ") %>]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
<% migrations.each do |m| -%>
|
|
65
|
+
def self.up_<%= m[:method_suffix] %>
|
|
66
|
+
[<%= m[:up_sql].map { |s| s.inspect }.join(", ") %>]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.down_<%= m[:method_suffix] %>
|
|
70
|
+
[<%= m[:down_sql].map { |s| s.inspect }.join(", ") %>]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
<% end -%>
|
|
74
|
+
|
|
75
|
+
# Dispatch on the migration name. Each branch is a typed,
|
|
76
|
+
# monomorphic Array<String> body — no dynamic method-name lookup
|
|
77
|
+
# (Spinel can't `send(:up_<name>)` portably).
|
|
78
|
+
def self.run_up(name = "")
|
|
79
|
+
<% migrations.each_with_index do |m, i| -%>
|
|
80
|
+
<%= i == 0 ? "if" : "elsif" %> name == "<%= m[:name] %>"
|
|
81
|
+
sqls = up_<%= m[:method_suffix] %>
|
|
82
|
+
j = 0
|
|
83
|
+
while j < sqls.length
|
|
84
|
+
Fresco::Db.exec(sqls[j])
|
|
85
|
+
j += 1
|
|
86
|
+
end
|
|
87
|
+
<% end -%>
|
|
88
|
+
<% if migrations.any? -%>
|
|
89
|
+
end
|
|
90
|
+
<% end -%>
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.run_down(name = "")
|
|
94
|
+
<% migrations.each_with_index do |m, i| -%>
|
|
95
|
+
<%= i == 0 ? "if" : "elsif" %> name == "<%= m[:name] %>"
|
|
96
|
+
sqls = down_<%= m[:method_suffix] %>
|
|
97
|
+
j = 0
|
|
98
|
+
while j < sqls.length
|
|
99
|
+
Fresco::Db.exec(sqls[j])
|
|
100
|
+
j += 1
|
|
101
|
+
end
|
|
102
|
+
<% end -%>
|
|
103
|
+
<% if migrations.any? -%>
|
|
104
|
+
end
|
|
105
|
+
<% end -%>
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Apply every pending migration in declaration order. Idempotent
|
|
109
|
+
# — already-applied names are skipped. Caller prints a header;
|
|
110
|
+
# this method prints one line per migration it actually ran.
|
|
111
|
+
def self.migrate!
|
|
112
|
+
ensure_schema_table!
|
|
113
|
+
<% if migrations.any? -%>
|
|
114
|
+
ns = names
|
|
115
|
+
i = 0
|
|
116
|
+
while i < ns.length
|
|
117
|
+
n = ns[i]
|
|
118
|
+
unless applied?(n)
|
|
119
|
+
run_up(n)
|
|
120
|
+
mark_applied!(n)
|
|
121
|
+
puts "[migrate] up " + n
|
|
122
|
+
end
|
|
123
|
+
i += 1
|
|
124
|
+
end
|
|
125
|
+
<% end -%>
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Roll back the most recently applied migration (the one at the
|
|
129
|
+
# end of the declaration list whose name appears in
|
|
130
|
+
# schema_migrations). Down-runs in declaration-reverse so the
|
|
131
|
+
# call from `./app db:rollback` undoes one step at a time —
|
|
132
|
+
# rerun the command to peel further back.
|
|
133
|
+
def self.rollback!
|
|
134
|
+
ensure_schema_table!
|
|
135
|
+
<% if migrations.any? -%>
|
|
136
|
+
ns = names
|
|
137
|
+
i = ns.length - 1
|
|
138
|
+
while i >= 0
|
|
139
|
+
n = ns[i]
|
|
140
|
+
if applied?(n)
|
|
141
|
+
run_down(n)
|
|
142
|
+
unmark!(n)
|
|
143
|
+
puts "[migrate] down " + n
|
|
144
|
+
return
|
|
145
|
+
end
|
|
146
|
+
i -= 1
|
|
147
|
+
end
|
|
148
|
+
<% end -%>
|
|
149
|
+
puts "[migrate] no migrations to roll back"
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|