@ender672/minja-js 0.1.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/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright 2024 Google LLC
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # @ender672/minja-js
2
+
3
+ Unofficial JavaScript port of [minja](https://github.com/ochafik/minja), a minimalistic Jinja2 template engine for LLM chat templates.
4
+
5
+ This is an independent port — not affiliated with or endorsed by the original minja project. The original C++ library is copyright 2024 Google LLC, licensed under MIT.
6
+
7
+ ## Usage
8
+
9
+ ### Low-level: parse and render a template
10
+
11
+ ```js
12
+ import { Parser, Context, parseTemplate } from '@ender672/minja-js/minja';
13
+
14
+ // parseTemplate() caches the AST so repeated calls with the same
15
+ // template string skip parsing. The AST is stateless — safe to
16
+ // render concurrently with different contexts.
17
+ const root = parseTemplate('Hello {{ name }}!');
18
+ const ctx = Context.make({ name: 'world' });
19
+ console.log(root.render(ctx)); // "Hello world!"
20
+ ```
21
+
22
+ If you are generating template strings dynamically and don't want to
23
+ fill the cache, call `Parser.parse()` directly instead.
24
+
25
+ ### High-level: ChatTemplate
26
+
27
+ ```js
28
+ import { ChatTemplate } from '@ender672/minja-js/chat-template';
29
+
30
+ const tmpl = new ChatTemplate(templateSource, bosToken, eosToken);
31
+ const output = tmpl.apply({
32
+ messages: [{ role: 'user', content: 'Hi' }],
33
+ addGenerationPrompt: true,
34
+ });
35
+ ```
36
+
37
+ `ChatTemplate` automatically caches its parsed AST via `parseTemplate()`,
38
+ so constructing multiple `ChatTemplate` instances with the same source
39
+ string only parses the template once.
40
+
41
+ ## Running tests
42
+
43
+ ```sh
44
+ npm test
45
+ ```
46
+
47
+ ## Differential fuzzing against C++ minja
48
+
49
+ To compare JS output against the C++ implementation:
50
+
51
+ ```sh
52
+ npm run build:diff-fuzz # one-time: clones C++ minja + nlohmann/json, compiles harness
53
+ npm run diff-fuzz # runs 10k iterations comparing C++ vs JS output
54
+ ```
55
+
56
+ Requires a C++ compiler with C++17 support, `curl`, and `git`. Dependencies are cached in `.deps/`.
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@ender672/minja-js",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "JavaScript port of the Minja Jinja2 template engine for LLM chat templates",
6
+ "license": "MIT",
7
+ "exports": {
8
+ "./minja": "./src/minja.js",
9
+ "./chat-template": "./src/chat-template.js"
10
+ },
11
+ "files": [
12
+ "src",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "test": "node --test test/*.js",
17
+ "build:diff-fuzz": "bash scripts/build-diff-fuzz-render.sh",
18
+ "diff-fuzz": "node scripts/diff-fuzz.js --cpp-bin scripts/diff-fuzz-render"
19
+ }
20
+ }
@@ -0,0 +1,535 @@
1
+ /*
2
+ Copyright 2024 Google LLC
3
+
4
+ Use of this source code is governed by an MIT-style
5
+ license that can be found in the LICENSE file or at
6
+ https://opensource.org/licenses/MIT.
7
+ */
8
+ // SPDX-License-Identifier: MIT
9
+
10
+ import { Parser, Context, Value, parseTemplate } from './minja.js';
11
+
12
+ export class ChatTemplate {
13
+ constructor(source, bosToken = '', eosToken = '') {
14
+ this._source = source;
15
+ this._bosToken = bosToken;
16
+ this._eosToken = eosToken;
17
+ this._templateRoot = parseTemplate(source, {
18
+ trimBlocks: true,
19
+ lstripBlocks: true,
20
+ keepTrailingNewline: false,
21
+ });
22
+ this._toolCallExample = '';
23
+ this._caps = {
24
+ supportsTools: false,
25
+ supportsToolCalls: false,
26
+ supportsToolResponses: false,
27
+ supportsSystemRole: false,
28
+ supportsParallelToolCalls: false,
29
+ supportsToolCallId: false,
30
+ requiresObjectArguments: false,
31
+ requiresNonNullContent: false,
32
+ requiresTypedContent: false,
33
+ };
34
+
35
+ this._detectCapabilities();
36
+ }
37
+
38
+ _tryRawRender(messages, tools, addGenerationPrompt, extraContext) {
39
+ try {
40
+ const inputs = {
41
+ messages,
42
+ tools,
43
+ addGenerationPrompt,
44
+ extraContext,
45
+ now: new Date(0), // epoch for tests
46
+ };
47
+ const opts = {
48
+ applyPolyfills: false,
49
+ };
50
+ return this.apply(inputs, opts);
51
+ } catch (e) {
52
+ return '';
53
+ }
54
+ }
55
+
56
+ _detectCapabilities() {
57
+ const contains = (haystack, needle) => haystack.includes(needle);
58
+
59
+ const userNeedle = '<User Needle>';
60
+ const sysNeedle = '<System Needle>';
61
+ const dummyStrUserMsg = { role: 'user', content: userNeedle };
62
+ const dummyTypedUserMsg = {
63
+ role: 'user',
64
+ content: [{ type: 'text', text: userNeedle }],
65
+ };
66
+
67
+ this._caps.requiresTypedContent =
68
+ !contains(this._tryRawRender([dummyStrUserMsg], undefined, false), userNeedle) &&
69
+ contains(this._tryRawRender([dummyTypedUserMsg], undefined, false), userNeedle);
70
+
71
+ const dummyUserMsg = this._caps.requiresTypedContent
72
+ ? dummyTypedUserMsg
73
+ : dummyStrUserMsg;
74
+ const needleSystemMsg = {
75
+ role: 'system',
76
+ content: this._caps.requiresTypedContent
77
+ ? [{ type: 'text', text: sysNeedle }]
78
+ : sysNeedle,
79
+ };
80
+
81
+ this._caps.supportsSystemRole = contains(
82
+ this._tryRawRender([needleSystemMsg, dummyUserMsg], undefined, false),
83
+ sysNeedle
84
+ );
85
+
86
+ let out = this._tryRawRender(
87
+ [dummyUserMsg],
88
+ [
89
+ {
90
+ name: 'some_tool',
91
+ type: 'function',
92
+ function: {
93
+ name: 'some_tool',
94
+ description: 'Some tool.',
95
+ parameters: {
96
+ type: 'object',
97
+ properties: {
98
+ arg: {
99
+ type: 'string',
100
+ description: 'Some argument.',
101
+ },
102
+ },
103
+ required: ['arg'],
104
+ },
105
+ },
106
+ },
107
+ ],
108
+ false
109
+ );
110
+ this._caps.supportsTools = contains(out, 'some_tool');
111
+
112
+ const renderWithContent = (content) => {
113
+ const assistantMsg = { role: 'assistant', content };
114
+ return this._tryRawRender(
115
+ [dummyUserMsg, assistantMsg, dummyUserMsg, assistantMsg],
116
+ undefined,
117
+ false
118
+ );
119
+ };
120
+ const outEmpty = renderWithContent('');
121
+ const outNull = renderWithContent(null);
122
+ this._caps.requiresNonNullContent =
123
+ contains(outEmpty, userNeedle) && !contains(outNull, userNeedle);
124
+
125
+ const makeToolCallsMsg = (toolCalls) => ({
126
+ role: 'assistant',
127
+ content: this._caps.requiresNonNullContent ? '' : null,
128
+ tool_calls: toolCalls,
129
+ });
130
+ const makeToolCall = (toolName, args) => ({
131
+ id: 'call_1___',
132
+ type: 'function',
133
+ function: {
134
+ arguments: args,
135
+ name: toolName,
136
+ },
137
+ });
138
+ const dummyArgsObj = { argument_needle: "print('Hello, World!')" };
139
+ const containsArgNeedle = (s) =>
140
+ contains(s, '<parameter=argument_needle>') ||
141
+ contains(s, '"argument_needle"') ||
142
+ contains(s, "'argument_needle':") ||
143
+ contains(s, '>argument_needle<');
144
+
145
+ // Test with string arguments
146
+ out = this._tryRawRender(
147
+ [
148
+ dummyUserMsg,
149
+ makeToolCallsMsg([makeToolCall('ipython', JSON.stringify(dummyArgsObj))]),
150
+ ],
151
+ undefined,
152
+ false
153
+ );
154
+ const toolCallRendersStrArguments = containsArgNeedle(out);
155
+
156
+ // Test with object arguments
157
+ out = this._tryRawRender(
158
+ [
159
+ dummyUserMsg,
160
+ makeToolCallsMsg([makeToolCall('ipython', dummyArgsObj)]),
161
+ ],
162
+ undefined,
163
+ false
164
+ );
165
+ const toolCallRendersObjArguments = containsArgNeedle(out);
166
+
167
+ this._caps.supportsToolCalls = toolCallRendersStrArguments || toolCallRendersObjArguments;
168
+ this._caps.requiresObjectArguments = !toolCallRendersStrArguments && toolCallRendersObjArguments;
169
+
170
+ if (this._caps.supportsToolCalls) {
171
+ const dummyArgs = this._caps.requiresObjectArguments
172
+ ? dummyArgsObj
173
+ : JSON.stringify(dummyArgsObj);
174
+ const tc1 = makeToolCall('test_tool1', dummyArgs);
175
+ const tc2 = makeToolCall('test_tool2', dummyArgs);
176
+ out = this._tryRawRender(
177
+ [dummyUserMsg, makeToolCallsMsg([tc1, tc2])],
178
+ undefined,
179
+ false
180
+ );
181
+ this._caps.supportsParallelToolCalls =
182
+ contains(out, 'test_tool1') && contains(out, 'test_tool2');
183
+
184
+ out = this._tryRawRender(
185
+ [
186
+ dummyUserMsg,
187
+ makeToolCallsMsg([tc1]),
188
+ {
189
+ role: 'tool',
190
+ name: 'test_tool1',
191
+ content: 'Some response!',
192
+ tool_call_id: 'call_911_',
193
+ },
194
+ ],
195
+ undefined,
196
+ false
197
+ );
198
+ this._caps.supportsToolResponses = contains(out, 'Some response!');
199
+ this._caps.supportsToolCallId = contains(out, 'call_911_');
200
+ }
201
+
202
+ // Generate tool call example for polyfill
203
+ try {
204
+ if (!this._caps.supportsTools) {
205
+ const userMsg = { role: 'user', content: 'Hey' };
206
+ const args = { arg1: 'some_value' };
207
+ const toolCallMsg = {
208
+ role: 'assistant',
209
+ content: this._caps.requiresNonNullContent ? '' : null,
210
+ tool_calls: [
211
+ {
212
+ id: 'call_1___',
213
+ type: 'function',
214
+ function: {
215
+ name: 'tool_name',
216
+ arguments: this._caps.requiresObjectArguments
217
+ ? args
218
+ : Value.fromJS(args).dump(-1, true),
219
+ },
220
+ },
221
+ ],
222
+ };
223
+
224
+ let prefix, full;
225
+ prefix = this.apply({ messages: [userMsg], addGenerationPrompt: true });
226
+ full = this.apply({
227
+ messages: [userMsg, toolCallMsg],
228
+ addGenerationPrompt: false,
229
+ });
230
+
231
+ let eosPos = full.lastIndexOf(this._eosToken);
232
+ if (
233
+ this._eosToken.length > 0 &&
234
+ (eosPos === prefix.length - this._eosToken.length ||
235
+ (full[full.length - 1] === '\n' &&
236
+ eosPos === full.length - this._eosToken.length - 1))
237
+ ) {
238
+ full = full.substring(0, eosPos);
239
+ }
240
+
241
+ let commonPrefixLength = 0;
242
+ for (let i = 0; i < prefix.length && i < full.length; i++) {
243
+ if (prefix[i] !== full[i]) break;
244
+ if (prefix[i] === '<') continue;
245
+ commonPrefixLength = i + 1;
246
+ }
247
+ const example = full.substring(commonPrefixLength);
248
+ if (example.includes('tool_name') || example.includes('some_value')) {
249
+ this._toolCallExample = example;
250
+ }
251
+ }
252
+ } catch (e) {
253
+ // Failed to generate tool call example
254
+ }
255
+ }
256
+
257
+ originalCaps() {
258
+ return { ...this._caps };
259
+ }
260
+
261
+ apply(inputs, options = {}) {
262
+ const {
263
+ messages = [],
264
+ tools,
265
+ addGenerationPrompt = true,
266
+ extraContext,
267
+ now = new Date(),
268
+ } = inputs;
269
+
270
+ const opts = {
271
+ applyPolyfills: options.applyPolyfills !== undefined ? options.applyPolyfills : true,
272
+ useBosToken: options.useBosToken !== undefined ? options.useBosToken : true,
273
+ useEosToken: options.useEosToken !== undefined ? options.useEosToken : true,
274
+ defineStrftimeNow: options.defineStrftimeNow !== undefined ? options.defineStrftimeNow : true,
275
+ polyfillTools: options.polyfillTools !== undefined ? options.polyfillTools : true,
276
+ polyfillToolCallExamples: options.polyfillToolCallExamples !== undefined ? options.polyfillToolCallExamples : true,
277
+ polyfillToolCalls: options.polyfillToolCalls !== undefined ? options.polyfillToolCalls : true,
278
+ polyfillToolResponses: options.polyfillToolResponses !== undefined ? options.polyfillToolResponses : true,
279
+ polyfillSystemRole: options.polyfillSystemRole !== undefined ? options.polyfillSystemRole : true,
280
+ polyfillObjectArguments: options.polyfillObjectArguments !== undefined ? options.polyfillObjectArguments : true,
281
+ polyfillTypedContent: options.polyfillTypedContent !== undefined ? options.polyfillTypedContent : true,
282
+ };
283
+
284
+ const hasTools = Array.isArray(tools) && tools.length > 0;
285
+ let hasToolCalls = false;
286
+ let hasToolResponses = false;
287
+ let hasStringContent = false;
288
+ for (const message of messages) {
289
+ if (message.tool_calls != null) hasToolCalls = true;
290
+ if (message.role === 'tool') hasToolResponses = true;
291
+ if (typeof message.content === 'string') hasStringContent = true;
292
+ }
293
+
294
+ const polyfillSystemRole = opts.polyfillSystemRole && !this._caps.supportsSystemRole;
295
+ const polyfillTools = opts.polyfillTools && hasTools && !this._caps.supportsTools;
296
+ const polyfillToolCallExample = polyfillTools && opts.polyfillToolCallExamples;
297
+ const polyfillToolCalls = opts.polyfillToolCalls && hasToolCalls && !this._caps.supportsToolCalls;
298
+ const polyfillToolResponses = opts.polyfillToolResponses && hasToolResponses && !this._caps.supportsToolResponses;
299
+ const polyfillObjectArguments = opts.polyfillObjectArguments && hasToolCalls && this._caps.requiresObjectArguments;
300
+ const polyfillTypedContent = opts.polyfillTypedContent && hasStringContent && this._caps.requiresTypedContent;
301
+
302
+ const needsPolyfills = opts.applyPolyfills && (
303
+ polyfillSystemRole ||
304
+ polyfillTools ||
305
+ polyfillToolCalls ||
306
+ polyfillToolResponses ||
307
+ polyfillObjectArguments ||
308
+ polyfillTypedContent
309
+ );
310
+
311
+ let actualMessages;
312
+
313
+ if (needsPolyfills) {
314
+ actualMessages = [];
315
+
316
+ const addMessage = (msg) => {
317
+ if (polyfillTypedContent && msg.content != null && typeof msg.content === 'string') {
318
+ actualMessages.push({
319
+ role: msg.role,
320
+ content: [{ type: 'text', text: msg.content }],
321
+ });
322
+ } else {
323
+ actualMessages.push(msg);
324
+ }
325
+ };
326
+
327
+ let pendingSystem = '';
328
+ const flushSys = () => {
329
+ if (pendingSystem.length > 0) {
330
+ addMessage({ role: 'user', content: pendingSystem });
331
+ pendingSystem = '';
332
+ }
333
+ };
334
+
335
+ let adjustedMessages;
336
+ if (polyfillTools) {
337
+ const toolsStr = Value.fromJS(tools).dump(2, true);
338
+ const exampleStr = polyfillToolCallExample && this._toolCallExample
339
+ ? '\n\nExample tool call syntax:\n\n' + this._toolCallExample + '\n\n'
340
+ : '';
341
+ adjustedMessages = ChatTemplate.addSystem(
342
+ messages,
343
+ "You can call any of the following tools to satisfy the user's requests: " +
344
+ toolsStr + exampleStr
345
+ );
346
+ } else {
347
+ adjustedMessages = messages;
348
+ }
349
+
350
+ for (let message of adjustedMessages) {
351
+ message = { ...message }; // shallow clone
352
+
353
+ if (message.tool_calls != null) {
354
+ if (polyfillObjectArguments || polyfillToolCalls) {
355
+ message.tool_calls = message.tool_calls.map((tc) => {
356
+ tc = { ...tc };
357
+ if (tc.type === 'function') {
358
+ tc.function = { ...tc.function };
359
+ if (typeof tc.function.arguments === 'string') {
360
+ try {
361
+ tc.function.arguments = JSON.parse(tc.function.arguments);
362
+ } catch (e) {
363
+ // Failed to parse arguments
364
+ }
365
+ }
366
+ }
367
+ return tc;
368
+ });
369
+ }
370
+ if (polyfillToolCalls) {
371
+ const toolCallsArr = [];
372
+ for (const tc of message.tool_calls) {
373
+ if (tc.type !== 'function') continue;
374
+ const entry = {
375
+ name: tc.function.name,
376
+ arguments: tc.function.arguments,
377
+ };
378
+ if (tc.id !== undefined) {
379
+ entry.id = tc.id;
380
+ }
381
+ toolCallsArr.push(entry);
382
+ }
383
+ const obj = { tool_calls: toolCallsArr };
384
+ if (message.content != null && message.content !== '') {
385
+ obj.content = message.content;
386
+ }
387
+ message.content = JSON.stringify(obj, null, 2);
388
+ delete message.tool_calls;
389
+ }
390
+ }
391
+
392
+ if (polyfillToolResponses && message.role === 'tool') {
393
+ message.role = 'user';
394
+ const toolResponse = {};
395
+ if (message.name !== undefined) {
396
+ toolResponse.tool = message.name;
397
+ }
398
+ toolResponse.content = message.content;
399
+ if (message.tool_call_id !== undefined) {
400
+ toolResponse.tool_call_id = message.tool_call_id;
401
+ }
402
+ message.content = JSON.stringify({ tool_response: toolResponse }, null, 2);
403
+ delete message.name;
404
+ }
405
+
406
+ if (message.content != null && polyfillSystemRole) {
407
+ const content = typeof message.content === 'string' ? message.content : JSON.stringify(message.content);
408
+ if (message.role === 'system') {
409
+ if (pendingSystem.length > 0) pendingSystem += '\n';
410
+ pendingSystem += content;
411
+ continue;
412
+ } else {
413
+ if (message.role === 'user') {
414
+ if (pendingSystem.length > 0) {
415
+ message = { ...message };
416
+ message.content = pendingSystem + (content.length === 0 ? '' : '\n' + content);
417
+ pendingSystem = '';
418
+ }
419
+ } else {
420
+ flushSys();
421
+ }
422
+ }
423
+ }
424
+ addMessage(message);
425
+ }
426
+ flushSys();
427
+ } else {
428
+ actualMessages = messages;
429
+ }
430
+
431
+ const contextData = {
432
+ messages: actualMessages,
433
+ add_generation_prompt: addGenerationPrompt,
434
+ };
435
+ const context = Context.make(contextData);
436
+ context.set('bos_token', opts.useBosToken ? this._bosToken : '');
437
+ context.set('eos_token', opts.useEosToken ? this._eosToken : '');
438
+
439
+ if (opts.defineStrftimeNow) {
440
+ const nowDate = now;
441
+ context.set(
442
+ 'strftime_now',
443
+ Value.callable((ctx, args) => {
444
+ args.expectArgs('strftime_now', [1, 1], [0, 0]);
445
+ const format = args.args[0].value;
446
+ return Value.fromJS(ChatTemplate._strftime(format, nowDate));
447
+ })
448
+ );
449
+ }
450
+
451
+ if (tools !== undefined && tools !== null) {
452
+ context.set('tools', Value.fromJS(tools));
453
+ }
454
+
455
+ if (extraContext != null) {
456
+ for (const [key, val] of Object.entries(extraContext)) {
457
+ context.set(key, Value.fromJS(val));
458
+ }
459
+ }
460
+
461
+ return this._templateRoot.render(context);
462
+ }
463
+
464
+ static addSystem(messages, systemPrompt) {
465
+ const result = [...messages];
466
+ if (result.length > 0 && result[0].role === 'system') {
467
+ result[0] = {
468
+ role: 'system',
469
+ content: result[0].content + '\n\n' + systemPrompt,
470
+ };
471
+ } else {
472
+ result.unshift({ role: 'system', content: systemPrompt });
473
+ }
474
+ return result;
475
+ }
476
+
477
+ static _strftime(format, date) {
478
+ const pad = (n, width = 2) => String(n).padStart(width, '0');
479
+ let result = '';
480
+ for (let i = 0; i < format.length; i++) {
481
+ if (format[i] === '%' && i + 1 < format.length) {
482
+ i++;
483
+ switch (format[i]) {
484
+ case 'Y': result += String(date.getFullYear()); break;
485
+ case 'm': result += pad(date.getMonth() + 1); break;
486
+ case 'd': result += pad(date.getDate()); break;
487
+ case 'H': result += pad(date.getHours()); break;
488
+ case 'M': result += pad(date.getMinutes()); break;
489
+ case 'S': result += pad(date.getSeconds()); break;
490
+ case 'j': {
491
+ const start = new Date(date.getFullYear(), 0, 0);
492
+ const diff = date - start;
493
+ const oneDay = 1000 * 60 * 60 * 24;
494
+ result += pad(Math.floor(diff / oneDay), 3);
495
+ break;
496
+ }
497
+ case 'p': result += date.getHours() >= 12 ? 'PM' : 'AM'; break;
498
+ case 'I': {
499
+ let h = date.getHours() % 12;
500
+ if (h === 0) h = 12;
501
+ result += pad(h);
502
+ break;
503
+ }
504
+ case 'A': {
505
+ const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
506
+ result += days[date.getDay()];
507
+ break;
508
+ }
509
+ case 'a': {
510
+ const daysShort = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
511
+ result += daysShort[date.getDay()];
512
+ break;
513
+ }
514
+ case 'B': {
515
+ const months = ['January', 'February', 'March', 'April', 'May', 'June',
516
+ 'July', 'August', 'September', 'October', 'November', 'December'];
517
+ result += months[date.getMonth()];
518
+ break;
519
+ }
520
+ case 'b': {
521
+ const monthsShort = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
522
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
523
+ result += monthsShort[date.getMonth()];
524
+ break;
525
+ }
526
+ case '%': result += '%'; break;
527
+ default: result += '%' + format[i]; break;
528
+ }
529
+ } else {
530
+ result += format[i];
531
+ }
532
+ }
533
+ return result;
534
+ }
535
+ }