@rhinostone/swig-django 2.7.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 ADDED
@@ -0,0 +1,57 @@
1
+ @rhinostone/swig-django
2
+ =======================
3
+
4
+ [![NPM version](https://img.shields.io/npm/v/@rhinostone/swig-django.svg?style=flat)](https://www.npmjs.com/package/@rhinostone/swig-django) [![Socket Badge](https://socket.dev/api/badge/npm/package/@rhinostone/swig-django)](https://socket.dev/npm/package/@rhinostone/swig-django)
5
+
6
+ Django Template Language frontend for the [@rhinostone/swig-core](https://www.npmjs.com/package/@rhinostone/swig-core) template engine. Part of the multi-flavor architecture introduced in `2.0.0` — see [ROADMAP.md](https://github.com/gina-io/swig/blob/develop/ROADMAP.md) for the release narrative.
7
+
8
+ Installation
9
+ ------------
10
+
11
+ npm install @rhinostone/swig-django
12
+
13
+ This pulls in `@rhinostone/swig-core` as a peer dependency, pinned to the matching version. Frontends and the core release in lockstep — do not mix versions.
14
+
15
+ Basic example
16
+ -------------
17
+
18
+ ```js
19
+ var swig = require('@rhinostone/swig-django');
20
+
21
+ var out = swig.render('Hello, {{ name|upper }}!', {
22
+ locals: { name: 'world' }
23
+ });
24
+ // => Hello, WORLD!
25
+ ```
26
+
27
+ Supported surface (as of 2.7.0)
28
+ -------------------------------
29
+
30
+ Renders real Django templates — everything below was cross-checked against Django 5.2.
31
+
32
+ * **Tags** — `if` / `elif` / `else`, `for` / `empty` (with `forloop`), `block`, `extends`, `include`, `with`, `autoescape`, `spaceless`, `comment`, `verbatim`, `cycle`, `firstof`.
33
+ * **Variable resolution** — Django-faithful attribute / dictionary / index lookup. Callable attributes are auto-called (honoring `alters_data` / `do_not_call_in_templates`), numeric indexing works (`{{ list.0 }}`), and dicts iterate via `.keys` / `.values` / `.items`.
34
+ * **Filter arguments** — Django colon syntax (`{{ value|date:"Y-m-d" }}`).
35
+ * **Filters** — 42 built-ins (`add`, `addslashes`, `capfirst`, `center`, `cut`, `date`, `default`, `default_if_none`, `divisibleby`, `escape` / `e`, `escapejs`, `filesizeformat`, `first`, `floatformat`, `force_escape`, `get_digit`, `iriencode`, `join`, `last`, `length`, `linebreaks`, `linebreaksbr`, `linenumbers`, `ljust`, `lower`, `make_list`, `phone2numeric`, `pluralize`, `rjust`, `safe`, `slice`, `slugify`, `striptags`, `time`, `title`, `truncatechars`, `truncatewords`, `upper`, `urlencode`, `wordcount`, `wordwrap`, `yesno`). See `lib/filters.js` for the full list.
36
+ * **Async** — `renderFileAsync` / `compileFileAsync` for async loaders, plus `renderFile(path, locals, cb)` against a loader with `async === true`.
37
+
38
+ Explicitly unsupported (parse-time throw or absent)
39
+ ---------------------------------------------------
40
+
41
+ * No sandboxed-rendering mode — template source is trusted.
42
+ * `{% load %}` and custom tag libraries — no code-loading.
43
+ * `{% url %}`, `{% static %}`, `{% csrf_token %}`, `{% trans %}`, `{% blocktrans %}` — framework infrastructure, out of scope.
44
+ * No whitespace-control (`{%- … -%}`) — not part of the Django Template Language.
45
+ * No `is` tests — Django's `is` is identity comparison, not Jinja2-style tests.
46
+ * `{{ block.super }}`, `{% for … reversed %}`, and one-variable `{% for x in dict.items %}` are deferred (clear parse-time errors).
47
+ * Deferred filters — `dictsort` / `dictsortreversed`, `stringformat`, `random`, `urlize` / `urlizetrunc`, `timesince` / `timeuntil`, `unordered_list`, `json_script`, `truncatechars_html` / `truncatewords_html`, `safeseq`, `pprint`.
48
+
49
+ Repository
50
+ ----------
51
+
52
+ Source: [gina-io/swig/packages/swig-django](https://github.com/gina-io/swig/tree/develop/packages/swig-django). File issues and PRs at [gina-io/swig](https://github.com/gina-io/swig).
53
+
54
+ License
55
+ -------
56
+
57
+ MIT. See [LICENSE](https://github.com/gina-io/swig/blob/develop/LICENSE) in the monorepo root.
@@ -0,0 +1,261 @@
1
+ var utils = require('@rhinostone/swig-core/lib/utils');
2
+
3
+ /*!
4
+ * Makes a string safe for a regular expression. Mirrors swig-django's parser.
5
+ * @private
6
+ */
7
+ function escapeRegExp(str) {
8
+ return str.replace(/[\-\/\\\^$*+?.()|\[\]{}]/g, '\\$&');
9
+ }
10
+
11
+ /*!
12
+ * Build the splitter regex from the controls trio. Mirrors the regex the
13
+ * Django parser builds at parse-time so the pre-walker chunks the same way
14
+ * the real parser would.
15
+ * @private
16
+ */
17
+ function buildSplitter(controls) {
18
+ var anyChar = '[\\s\\S]*?',
19
+ varOpen = escapeRegExp(controls.varControls[0]),
20
+ varClose = escapeRegExp(controls.varControls[1]),
21
+ tagOpen = escapeRegExp(controls.tagControls[0]),
22
+ tagClose = escapeRegExp(controls.tagControls[1]),
23
+ cmtOpen = escapeRegExp(controls.cmtControls[0]),
24
+ cmtClose = escapeRegExp(controls.cmtControls[1]);
25
+ return new RegExp(
26
+ '(' +
27
+ tagOpen + anyChar + tagClose + '|' +
28
+ varOpen + anyChar + varClose + '|' +
29
+ cmtOpen + anyChar + cmtClose +
30
+ ')'
31
+ );
32
+ }
33
+
34
+ /*!
35
+ * Strip tag controls from a tag chunk, returning the trimmed tag body (e.g.
36
+ * <code>extends "x.html"</code>). Django has no whitespace-control markers,
37
+ * so there is nothing to strip beyond the controls + surrounding space.
38
+ * @private
39
+ */
40
+ function stripTagBody(chunk, tagOpen, tagClose) {
41
+ var body = chunk.substr(tagOpen.length, chunk.length - tagOpen.length - tagClose.length);
42
+ return body.replace(/^\s+|\s+$/g, '');
43
+ }
44
+
45
+ /**
46
+ * Scan Django template source for static <code>{% extends|include "..." %}</code>
47
+ * targets. Pure function; performs no I/O.
48
+ *
49
+ * The scanner mirrors the Django parser's chunk-splitter so it agrees on
50
+ * chunk boundaries even under non-default control characters. Dynamic
51
+ * paths (<code>{% extends parent_var %}</code>) and tag bodies whose
52
+ * first token isn't a string literal are silently skipped — they remain
53
+ * on the sync path, which throws appropriately at parse time.
54
+ *
55
+ * @example
56
+ * preWalker.scan('{% include "partial.html" %}', {
57
+ * varControls: ['{{', '}}'],
58
+ * tagControls: ['{%', '%}'],
59
+ * cmtControls: ['{#', '#}'],
60
+ * rawTag: 'verbatim',
61
+ * keywords: ['extends', 'include']
62
+ * });
63
+ * // => [{ kind: 'include', path: 'partial.html' }]
64
+ *
65
+ * @param {string} source
66
+ * @param {object} opts
67
+ * @param {array} opts.varControls
68
+ * @param {array} opts.tagControls
69
+ * @param {array} opts.cmtControls
70
+ * @param {string} opts.rawTag Tag name that opens verbatim regions
71
+ * (<code>verbatim</code> for Django).
72
+ * @param {array} opts.keywords Keywords whose first quoted argument
73
+ * is a template path. Django:
74
+ * <code>['extends', 'include']</code>.
75
+ * @return {array} List of <code>{ kind, path }</code> entries.
76
+ */
77
+ exports.scan = function (source, opts) {
78
+ source = source.replace(/\r\n/g, '\n');
79
+
80
+ var splitter = buildSplitter(opts),
81
+ tagOpen = opts.tagControls[0],
82
+ tagClose = opts.tagControls[1],
83
+ rawTag = opts.rawTag,
84
+ endRawTag = 'end' + rawTag,
85
+ keywordRegex = new RegExp(
86
+ '^(' + opts.keywords.join('|') + ')\\s+["\\\']([^"\\\']+)["\\\']'
87
+ ),
88
+ chunks = source.split(splitter),
89
+ results = [],
90
+ inRaw = false,
91
+ i,
92
+ chunk,
93
+ body,
94
+ name,
95
+ m;
96
+
97
+ for (i = 0; i < chunks.length; i += 1) {
98
+ chunk = chunks[i];
99
+ if (typeof chunk !== 'string' || !chunk) {
100
+ continue;
101
+ }
102
+
103
+ if (!utils.startsWith(chunk, tagOpen) || !utils.endsWith(chunk, tagClose)) {
104
+ continue;
105
+ }
106
+
107
+ body = stripTagBody(chunk, tagOpen, tagClose);
108
+ name = body.split(/\s+/)[0];
109
+
110
+ if (name === rawTag) {
111
+ inRaw = true;
112
+ continue;
113
+ }
114
+ if (name === endRawTag) {
115
+ inRaw = false;
116
+ continue;
117
+ }
118
+ if (inRaw) {
119
+ continue;
120
+ }
121
+
122
+ m = keywordRegex.exec(body);
123
+ if (m) {
124
+ results.push({ kind: m[1], path: m[2] });
125
+ }
126
+ }
127
+
128
+ return results;
129
+ };
130
+
131
+ /**
132
+ * Walk the dependency graph asynchronously starting from <var>entryPath</var>.
133
+ *
134
+ * Repeatedly loads, scans, and resolves child template paths in parallel
135
+ * via the user's async loader, until the dep graph closes. Returns a
136
+ * Promise resolving to a populated <code>{ resolvedPath: source }</code>
137
+ * map suitable for backing a memory loader.
138
+ *
139
+ * Cycles in the graph are tolerated — once a path is in the map or
140
+ * pending, subsequent enqueue requests are dropped. The synchronous
141
+ * renderer's existing circular-extends guard handles cycles at parse
142
+ * time on the second pass.
143
+ *
144
+ * @param {string} entryPath Resolved path of the entry template.
145
+ * @param {object} loader User loader. Must expose:
146
+ * <code>resolve(to, from)</code> (sync, returns
147
+ * string) and
148
+ * <code>load(id, cb)</code> (async, calls
149
+ * <code>cb(err, source)</code>).
150
+ * @param {object} scanOpts Pass-through to {@link scan}.
151
+ * @return {Promise} Resolves to the populated memory map.
152
+ */
153
+ exports.walk = function (entryPath, loader, scanOpts) {
154
+ var memMap = {};
155
+ var pending = {};
156
+
157
+ return new Promise(function (resolve, reject) {
158
+ var inFlight = 0;
159
+ var queue = [];
160
+ var hasError = false;
161
+
162
+ function enqueue(path) {
163
+ if (memMap.hasOwnProperty(path) || pending[path]) {
164
+ return;
165
+ }
166
+ pending[path] = true;
167
+ queue.push(path);
168
+ }
169
+
170
+ function drain() {
171
+ while (queue.length > 0 && !hasError) {
172
+ var path = queue.shift();
173
+ inFlight += 1;
174
+ startLoad(path);
175
+ }
176
+ if (inFlight === 0 && !hasError && queue.length === 0) {
177
+ resolve(memMap);
178
+ }
179
+ }
180
+
181
+ function startLoad(resolvedPath) {
182
+ loader.load(resolvedPath, function (err, src) {
183
+ if (hasError) {
184
+ return;
185
+ }
186
+ if (err) {
187
+ hasError = true;
188
+ reject(err);
189
+ return;
190
+ }
191
+ if (typeof src !== 'string') {
192
+ hasError = true;
193
+ reject(new Error('Async loader returned non-string source for "' + resolvedPath + '"'));
194
+ return;
195
+ }
196
+ memMap[resolvedPath] = src;
197
+
198
+ var targets;
199
+ try {
200
+ targets = exports.scan(src, scanOpts);
201
+ } catch (e) {
202
+ hasError = true;
203
+ reject(e);
204
+ return;
205
+ }
206
+
207
+ var i, resolvedChild;
208
+ for (i = 0; i < targets.length; i += 1) {
209
+ try {
210
+ resolvedChild = loader.resolve(targets[i].path, resolvedPath);
211
+ } catch (e) {
212
+ hasError = true;
213
+ reject(e);
214
+ return;
215
+ }
216
+ enqueue(resolvedChild);
217
+ }
218
+
219
+ inFlight -= 1;
220
+ drain();
221
+ });
222
+ }
223
+
224
+ enqueue(entryPath);
225
+ drain();
226
+ });
227
+ };
228
+
229
+ /**
230
+ * Build a sync memory wrapper around a pre-populated
231
+ * <code>{ resolvedPath: source }</code> map. Delegates <code>resolve</code>
232
+ * to the user loader so cache keys match what the pre-walker produced.
233
+ *
234
+ * @param {object} userLoader Original async loader (used for resolve).
235
+ * @param {object} memMap Pre-populated source map.
236
+ * @return {object} A loader exposing <code>resolve</code> and
237
+ * <code>load</code>.
238
+ */
239
+ exports.makeMemoryWrapper = function (userLoader, memMap) {
240
+ return {
241
+ resolve: function (to, from) {
242
+ return userLoader.resolve(to, from);
243
+ },
244
+ load: function (id, cb) {
245
+ var src = memMap[id];
246
+ if (typeof src !== 'string') {
247
+ var err = new Error('Pre-walked map missing path: "' + id + '"');
248
+ if (cb) {
249
+ cb(err);
250
+ return;
251
+ }
252
+ throw err;
253
+ }
254
+ if (cb) {
255
+ cb(null, src);
256
+ return;
257
+ }
258
+ return src;
259
+ }
260
+ };
261
+ };