sottolio 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/Gemfile +2 -0
  4. data/Gemfile.lock +31 -0
  5. data/LICENSE +676 -0
  6. data/README.md +26 -0
  7. data/Rakefile +12 -0
  8. data/bin/sottolio +29 -0
  9. data/example/game/include/CanvasText-0.4.js +500 -0
  10. data/example/game/include/canvasinput.js +379 -0
  11. data/example/game/include/howler.min.js +10 -0
  12. data/example/game/include/jquery.min.js +4 -0
  13. data/example/game/index.html +19 -0
  14. data/example/game/resources/backgrounds/city.jpg +0 -0
  15. data/example/game/resources/characters/ambrogia.png +0 -0
  16. data/example/game/resources/characters/rosalinda.png +0 -0
  17. data/example/game/resources/right_arrow.png +0 -0
  18. data/example/game/resources/sounds/Classmate.m4a +0 -0
  19. data/example/scripts/chapter_1.rb +64 -0
  20. data/example/scripts/chapter_2.rb +23 -0
  21. data/example/scripts/script.rb +2 -0
  22. data/lib/sottolio.rb +19 -0
  23. data/lib/sottolio/sottolio.rb +22 -0
  24. data/lib/sottolio/version.rb +21 -0
  25. data/opal/sottolio.rb +38 -0
  26. data/opal/sottolio/application.rb +134 -0
  27. data/opal/sottolio/database.rb +48 -0
  28. data/opal/sottolio/image_manager.rb +46 -0
  29. data/opal/sottolio/lock.rb +41 -0
  30. data/opal/sottolio/script.rb +93 -0
  31. data/opal/sottolio/sottolio.rb +29 -0
  32. data/opal/sottolio/sound_manager.rb +55 -0
  33. data/opal/sottolio/utils.rb +57 -0
  34. data/opal/sottolio/wrapper/background.rb +21 -0
  35. data/opal/sottolio/wrapper/canvas.rb +90 -0
  36. data/opal/sottolio/wrapper/canvas/canvas_button.rb +69 -0
  37. data/opal/sottolio/wrapper/canvas/canvas_input.rb +73 -0
  38. data/opal/sottolio/wrapper/canvas/canvas_text.rb +64 -0
  39. data/opal/sottolio/wrapper/character.rb +21 -0
  40. data/opal/sottolio/wrapper/image.rb +65 -0
  41. data/opal/sottolio/wrapper/sound.rb +55 -0
  42. data/sottolio.gemspec +20 -0
  43. metadata +114 -0
data/README.md ADDED
@@ -0,0 +1,26 @@
1
+ Sottolio - È sopraffino
2
+ ========================
3
+ Porting of [Sottaceto](https://github.com/RoxasShadow/Sottaceto) in JavaScript written in Ruby thanks to Opal.
4
+
5
+ Just like Sottaceto, sottolio is a game engine to create visual novels with ease. These games run everywhere, you only need a~~~n internet browser which supports JavaScript and HTML5~~~ decent internet browser.
6
+
7
+ The scripts (check `compiler/scripts`) are pretty self-explanatory (even more than Sottaceto's ones, actually).
8
+
9
+ Backgrounds, musics, and other stuff are kept inside `game/resources/`.
10
+
11
+ Setup
12
+ =====
13
+ `$ gem install sottolio`
14
+
15
+ `$ npm install -g uglify-js`
16
+
17
+ Run the demo
18
+ ============
19
+ `$ sottolio example/scripts example/game/sottolio`
20
+
21
+
22
+ sottolio will generate `sottolio.js` and `sottolio.min.js` inside `example/game` that are nothing but the compiled version of the scripts inside `example/scripts`.
23
+
24
+ You're now ready to open `example/game/index.html` in your browser!
25
+
26
+ The [demo](http://www.giovannicapuano.net/sottolio/) is also available in the web, as well the [video gameplay](http://www.youtube.com/watch?v=djV_Z5OeBmg&feature=youtu.be) (it's a bit old tho).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rake'
3
+
4
+ task default: [ :build, :install ]
5
+
6
+ task :build do
7
+ sh 'gem build sottolio.gemspec'
8
+ end
9
+
10
+ task :install do
11
+ sh 'gem install *.gem'
12
+ end
data/bin/sottolio ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ #--
3
+ # Copyright(C) 2013-2015 Giovanni Capuano <webmaster@giovannicapuano.net>
4
+ #
5
+ # This file is part of sottolio.
6
+ #
7
+ # sottolio is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # sottolio is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with sottolio. If not, see <http://www.gnu.org/licenses/>.
19
+ #++
20
+ require 'sottolio'
21
+
22
+ abort 'Usage: sottolio <scripts> <output>' if ARGV.length != 2
23
+
24
+ Opal.append_path ARGV[0]
25
+
26
+ build = Opal::Builder.build('sottolio').to_s
27
+
28
+ File.write "#{ARGV[1]}.js", build
29
+ File.write "#{ARGV[1]}.min.js", Opal::Util.uglify(build)
@@ -0,0 +1,500 @@
1
+ /**
2
+ * Copyright (c) 2011 Pere Monfort Pàmies (http://www.pmphp.net)
3
+ * Official site: http://www.canvastext.com
4
+ *
5
+ * Permission is hereby granted, free of charge, to any person obtaining a
6
+ * copy of this software and associated documentation files (the
7
+ * "Software"), to deal in the Software without restriction, including
8
+ * without limitation the rights to use, copy, modify, merge, publish,
9
+ * distribute, sublicense, and/or sell copies of the Software, and to permit
10
+ * persons to whom the Software is furnished to do so, subject to the
11
+ * following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included
14
+ * in all copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
19
+ * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
20
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
21
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
22
+ * USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+ */
24
+ function CanvasText() {
25
+ // The property that will contain the ID attribute value.
26
+ this.canvasId = null;
27
+ // The property that will contain the Canvas element.
28
+ this.canvas = null;
29
+ // The property that will contain the canvas context.
30
+ this.context = null;
31
+ // The property that will contain the buffer/cache canvas.
32
+ this.bufferCanvas = null;
33
+ // The property that will contain the cacheCanvas context.
34
+ this.bufferContext = null;
35
+ // The property that will contain all cached canvas.
36
+ this.cacheCanvas = [];
37
+ // The property that will contain all cached contexts.
38
+ this.cacheContext = [];
39
+ // The property that will contain the created style class.
40
+ this.savedClasses = [];
41
+
42
+ /*
43
+ * Default values.
44
+ */
45
+ this.fontFamily = "Verdana";
46
+ this.fontWeight = "normal";
47
+ this.fontSize = "12px";
48
+ this.fontColor = "#000";
49
+ this.fontStyle = "normal";
50
+ this.textAlign = "start";
51
+ this.textBaseline = "alphabetic";
52
+ this.lineHeight = "16";
53
+ this.textShadow = null;
54
+
55
+ /**
56
+ * Benckmark variables.
57
+ */
58
+ this.initTime = null;
59
+ this.endTime = null;
60
+
61
+ /**
62
+ * Set the main values.
63
+ * @param object config Text properties.
64
+ */
65
+ this.config = function (config) {
66
+ var property;
67
+ /*
68
+ * A simple check. If config is not an object popup an alert.
69
+ */
70
+ if (typeof (config) !== "object") {
71
+ alert("¡Invalid configuration!");
72
+ return false;
73
+ }
74
+ /*
75
+ * Loop the config properties.
76
+ */
77
+ for (property in config) {
78
+ // If it's a valid property, save it.
79
+ if (this[property] !== undefined) {
80
+ this[property] = config[property];
81
+ }
82
+ }
83
+ };
84
+
85
+ /**
86
+ * @param object textInfo Contains the Text string, axis X value and axis Y value.
87
+ */
88
+ this.drawText = function (textInfo) {
89
+ this.initTime = new Date().getTime();
90
+ /*
91
+ * If this.canvas doesn't exist we will try to set it.
92
+ * This will be done the first execution time.
93
+ */
94
+ if (this.canvas == null) {
95
+ if (!this.getCanvas()) {
96
+ alert("Incorrect canvas ID!");
97
+ return false;
98
+ }
99
+ }
100
+ /**
101
+ *
102
+ */
103
+ if (this.bufferCanvas == null) {
104
+ this.getBufferCanvas();
105
+ }
106
+ /**
107
+ * Get or set the cache if a cacheId exist.
108
+ */
109
+ if (textInfo.cacheId !== undefined) {
110
+ // We add a prefix to avoid name conflicts.
111
+ textInfo.cacheId = "ct" + textInfo.cacheId;
112
+ // If cache exists: draw text and stop script execution.
113
+ if (this.getCache(textInfo.cacheId)) {
114
+ if (!textInfo.returnImage) {
115
+ this.context.drawImage(this.cacheCanvas[textInfo.cacheId], 0, 0);
116
+ } else if (textInfo.returnImage) {
117
+ return this.cacheCanvas[textInfo.cacheId];
118
+ }
119
+
120
+ this.endTime = new Date().getTime();
121
+ //console.log("cache",(this.endTime-this.initTime)/1000);
122
+ return false;
123
+ }
124
+ }
125
+ // A simple check.
126
+ if (typeof (textInfo) != "object") {
127
+ alert("Invalid text format!");
128
+ return false;
129
+ }
130
+ // Another simple check
131
+ if (!this.isNumber(textInfo.x) || !this.isNumber(textInfo.y)) {
132
+ alert("You should specify a correct \"x\" & \"y\" axis value.");
133
+ return false;
134
+ }
135
+ // Reset our cacheCanvas.
136
+ this.bufferCanvas.width = this.bufferCanvas.width;
137
+ // Set the color.
138
+ this.bufferContext.fillStyle = this.fontColor;
139
+ // Set the size & font family.
140
+ this.bufferContext.font = this.fontWeight + ' ' + this.fontSize + ' ' + this.fontFamily;
141
+ // Parse and draw the styled text.
142
+ this.drawStyledText(textInfo);
143
+ // Cache the result.
144
+ if (textInfo.cacheId != undefined) {
145
+ this.setCache(textInfo.cacheId);
146
+ }
147
+
148
+ this.endTime = new Date().getTime();
149
+ //console.log((this.endTime-this.initTime)/1000);
150
+ // Draw or return the final image.
151
+ if (!textInfo.returnImage) {
152
+ this.context.drawImage(this.bufferCanvas, 0, 0);
153
+ } else if (textInfo.returnImage) {
154
+ return this.bufferCanvas;
155
+ }
156
+ };
157
+
158
+ /**
159
+ * The "painter". This will draw the styled text.
160
+ */
161
+ this.drawStyledText = function (textInfo) {
162
+ // Save the textInfo into separated vars to work more comfortably.
163
+ var text = textInfo.text, x = textInfo.x, y = textInfo.y;
164
+ // Needed vars for automatic line break;
165
+ var splittedText, xAux, textLines = [], boxWidth = textInfo.boxWidth;
166
+ // Declaration of needed vars.
167
+ var proFont = [], properties, property, propertyName, propertyValue, atribute;
168
+ var classDefinition, proColor, proText, proShadow;
169
+ // Loop vars
170
+ var i, j, k, n;
171
+
172
+ // The main regex. Looks for <style>, <class> or <br /> tags.
173
+ var match = text.match(/<\s*br\s*\/>|<\s*class=["|']([^"|']+)["|']\s*\>([^>]+)<\s*\/class\s*\>|<\s*style=["|']([^"|']+)["|']\s*\>([^>]+)<\s*\/style\s*\>|[^<]+/g);
174
+ var innerMatch = null;
175
+
176
+ // Let's draw something for each match found.
177
+ for (i = 0; i < match.length; i++) {
178
+ // Save the current context.
179
+ this.bufferContext.save();
180
+ // Default color
181
+ proColor = this.fontColor;
182
+ // Default font
183
+ proFont.style = this.fontStyle;
184
+ proFont.weight = this.fontWeight;
185
+ proFont.size = this.fontSize;
186
+ proFont.family = this.fontFamily;
187
+
188
+ // Default shadow
189
+ proShadow = this.textShadow;
190
+
191
+ // Check if current fragment is an style tag.
192
+ if (/<\s*style=/i.test(match[i])) {
193
+ // Looks the attributes and text inside the style tag.
194
+ innerMatch = match[i].match(/<\s*style=["|']([^"|']+)["|']\s*\>([^>]+)<\s*\/style\s*\>/);
195
+
196
+ // innerMatch[1] contains the properties of the attribute.
197
+ properties = innerMatch[1].split(";");
198
+
199
+ // Apply styles for each property.
200
+ for (j = 0; j < properties.length; j++) {
201
+ // Each property have a value. We split them.
202
+ property = properties[j].split(":");
203
+ // A simple check.
204
+ if (this.isEmpty(property[0]) || this.isEmpty(property[1])) {
205
+ // Wrong property name or value. We jump to the
206
+ // next loop.
207
+ continue;
208
+ }
209
+ // Again, save it into friendly-named variables to work comfortably.
210
+ propertyName = property[0];
211
+ propertyValue = property[1];
212
+
213
+ switch (propertyName) {
214
+ case "font":
215
+ proFont = propertyValue;
216
+ break;
217
+ case "font-family":
218
+ proFont.family = propertyValue;
219
+ break;
220
+ case "font-weight":
221
+ proFont.weight = propertyValue;
222
+ break;
223
+ case "font-size":
224
+ proFont.size = propertyValue;
225
+ break;
226
+ case "font-style":
227
+ proFont.style = propertyValue;
228
+ break;
229
+ case "text-shadow":
230
+ proShadow = this.trim(propertyValue);
231
+ proShadow = proShadow.split(" ");
232
+ if (proShadow.length != 4) {
233
+ proShadow = null;
234
+ }
235
+ break;
236
+ case "color":
237
+ if (this.isHex(propertyValue)) {
238
+ proColor = propertyValue;
239
+ }
240
+ break;
241
+ }
242
+ }
243
+ proText = innerMatch[2];
244
+
245
+ } else if (/<\s*class=/i.test(match[i])) { // Check if current fragment is a class tag.
246
+ // Looks the attributes and text inside the class tag.
247
+ innerMatch = match[i].match(/<\s*class=["|']([^"|']+)["|']\s*\>([^>]+)<\s*\/class\s*\>/);
248
+
249
+ classDefinition = this.getClass(innerMatch[1]);
250
+ /*
251
+ * Loop the class properties.
252
+ */
253
+ for (atribute in classDefinition) {
254
+ switch (atribute) {
255
+ case "font":
256
+ proFont = classDefinition[atribute];
257
+ break;
258
+ case "fontFamily":
259
+ proFont.family = classDefinition[atribute];
260
+ break;
261
+ case "fontWeight":
262
+ proFont.weight = classDefinition[atribute];
263
+ break;
264
+ case "fontSize":
265
+ proFont.size = classDefinition[atribute];
266
+ break;
267
+ case "fontStyle":
268
+ proFont.style = classDefinition[atribute];
269
+ break;
270
+ case "fontColor":
271
+ if (this.isHex(classDefinition[atribute])) {
272
+ proColor = classDefinition[atribute];
273
+ }
274
+ break;
275
+ case "textShadow":
276
+ proShadow = this.trim(classDefinition[atribute]);
277
+ proShadow = proShadow.split(" ");
278
+ if (proShadow.length != 4) {
279
+ proShadow = null;
280
+ }
281
+ break;
282
+ }
283
+ }
284
+ proText = innerMatch[2];
285
+ } else if (/<\s*br\s*\/>/i.test(match[i])) {
286
+ // Check if current fragment is a line break.
287
+ y += parseInt(this.lineHeight, 10) * 1.5;
288
+ x = textInfo.x;
289
+ continue;
290
+ } else {
291
+ // Text without special style.
292
+ proText = match[i];
293
+ }
294
+
295
+ // Set the text Baseline
296
+ this.bufferContext.textBaseline = this.textBaseline;
297
+ // Set the text align
298
+ this.bufferContext.textAlign = this.textAlign;
299
+ // Font styles.
300
+ if (proFont instanceof Array) {
301
+ this.bufferContext.font = proFont.style + " " + proFont.weight + " " + proFont.size + " " + proFont.family;
302
+ } else {
303
+ this.bufferContext.font = proFont;
304
+ }
305
+ this.bufferContext.font = proFont;
306
+ // Set the color.
307
+ this.bufferContext.fillStyle = proColor;
308
+ // Set the Shadow.
309
+ if (proShadow != null) {
310
+ this.bufferContext.shadowOffsetX = proShadow[0].replace("px", "");
311
+ this.bufferContext.shadowOffsetY = proShadow[1].replace("px", "");
312
+ this.bufferContext.shadowBlur = proShadow[2].replace("px", "");
313
+ this.bufferContext.shadowColor = proShadow[3].replace("px", "");
314
+ }
315
+
316
+ // Reset textLines;
317
+ textLines = [];
318
+ // Clear javascript code line breaks.
319
+ proText = proText.replace(/\s*\n\s*/g, " ");
320
+
321
+ // Automatic Line break
322
+ if (boxWidth !== undefined) {
323
+ // If returns true, it means we need a line break.
324
+ if (this.checkLineBreak(proText, boxWidth, x)) {
325
+ // Split text by words.
326
+ splittedText = this.trim(proText).split(" ");
327
+
328
+ // If there's only one word we don't need to make more checks.
329
+ if (splittedText.length == 1) {
330
+ textLines.push({text: this.trim(proText) + " ", linebreak: true});
331
+ } else {
332
+ // Reset vars.
333
+ xAux = x;
334
+ var line = 0;
335
+ textLines[line] = {text: undefined, linebreak: false};
336
+
337
+ // Loop words.
338
+ for (k = 0; k < splittedText.length; k++) {
339
+ splittedText[k] += " ";
340
+ // Check if the current text fits into the current line.
341
+ if (!this.checkLineBreak(splittedText[k], boxWidth, xAux)) {
342
+ // Current text fit into the current line. So we save it
343
+ // to the current textLine.
344
+ if (textLines[line].text == undefined) {
345
+ textLines[line].text = splittedText[k];
346
+ } else {
347
+ textLines[line].text += splittedText[k];
348
+ }
349
+
350
+ xAux += this.bufferContext.measureText(splittedText[k]).width;
351
+ } else {
352
+ // Current text doesn't fit into the current line.
353
+ // We are doing a line break, so we reset xAux
354
+ // to initial x value.
355
+ xAux = textInfo.x;
356
+ if (textLines[line].text !== undefined) {
357
+ line++;
358
+ }
359
+
360
+ textLines[line] = {text: splittedText[k], linebreak: true};
361
+ xAux += this.bufferContext.measureText(splittedText[k]).width;
362
+ }
363
+ }
364
+ }
365
+ }
366
+ }
367
+
368
+ // if textLines.length == 0 it means we doesn't need a linebreak.
369
+ if (textLines.length == 0) {
370
+ textLines.push({text: this.trim(proText) + " ", linebreak: false});
371
+ }
372
+
373
+ // Let's draw the text
374
+ for (n = 0; n < textLines.length; n++) {
375
+ // Start a new line.
376
+ if (textLines[n].linebreak) {
377
+ y += parseInt(this.lineHeight, 10);
378
+ x = textInfo.x;
379
+ }
380
+ this.bufferContext.fillText(textLines[n].text, x, y);
381
+ // Increment X position based on current text measure.
382
+ x += this.bufferContext.measureText(textLines[n].text).width;
383
+ }
384
+
385
+ this.bufferContext.restore();
386
+ }
387
+ };
388
+
389
+ /**
390
+ * Save a new class definition.
391
+ */
392
+ this.defineClass = function (id, definition) {
393
+ // A simple check.
394
+ if (typeof (definition) != "object") {
395
+ alert("¡Invalid class!");
396
+ return false;
397
+ }
398
+ // Another simple check.
399
+ if (this.isEmpty(id)) {
400
+ alert("You must specify a Class Name.");
401
+ return false;
402
+ }
403
+
404
+ // Save it.
405
+ this.savedClasses[id] = definition;
406
+ return true;
407
+ };
408
+
409
+ /**
410
+ * Returns a saved class.
411
+ */
412
+ this.getClass = function (id) {
413
+ if (this.savedClasses[id] !== undefined) {
414
+ return this.savedClasses[id];
415
+ }
416
+ };
417
+
418
+ this.getCanvas = function () {
419
+ // We need a valid ID
420
+ if (this.canvasId == null) {
421
+ alert("You must specify the canvas ID!");
422
+ return false;
423
+ }
424
+ // Let's save the Canvas into our class property...
425
+ this.canvas = document.getElementById(this.canvasId);
426
+ // ... and save its context too.
427
+ this.context = this.canvas.getContext('2d');
428
+ this.getBufferCanvas();
429
+
430
+ return true;
431
+ };
432
+
433
+ this.getBufferCanvas = function () {
434
+ // We will draw the text into the cache canvas
435
+ this.bufferCanvas = document.createElement('canvas');
436
+ this.bufferCanvas.width = this.canvas.width;
437
+ this.bufferCanvas.height = this.canvas.height;
438
+ this.bufferContext = this.bufferCanvas.getContext('2d');
439
+ };
440
+ /**
441
+ * Check if the cache canvas exist.
442
+ */
443
+ this.getCache = function (id) {
444
+ if (this.cacheCanvas[id] === undefined) {
445
+ return false;
446
+ } else {
447
+ return true;
448
+ }
449
+ };
450
+ /**
451
+ * We create a new canvas element for each cache element.
452
+ */
453
+ this.setCache = function (id) {
454
+ this.cacheCanvas[id] = document.createElement("canvas");
455
+ this.cacheCanvas[id].width = this.bufferCanvas.width;
456
+ this.cacheCanvas[id].height = this.bufferCanvas.height;
457
+ this.cacheContext[id] = this.cacheCanvas[id].getContext('2d');
458
+ this.cacheContext[id].drawImage(this.bufferCanvas, 0, 0);
459
+ };
460
+ /**
461
+ * Check if a line break is needed.
462
+ */
463
+ this.checkLineBreak = function (text, boxWidth, x) {
464
+ return (this.bufferContext.measureText(text).width + x > boxWidth);
465
+ };
466
+
467
+ /**
468
+ * A simple function to validate a Hex code.
469
+ */
470
+ this.isHex = function (hex) {
471
+ return (/^(#[a-fA-F0-9]{3,6})$/i.test(hex));
472
+ };
473
+ /**
474
+ * A simple function to check if the given value is a number.
475
+ */
476
+ this.isNumber = function (n) {
477
+ return !isNaN(parseFloat(n)) && isFinite(n);
478
+ };
479
+ /**
480
+ * A simple function to check if the given value is empty.
481
+ */
482
+ this.isEmpty = function (str) {
483
+ // Remove white spaces.
484
+ str = str.replace(/^\s+|\s+$/, '');
485
+ return str.length == 0;
486
+ };
487
+ /**
488
+ * A simple function clear whitespaces.
489
+ */
490
+ this.trim = function (str) {
491
+ var ws, i;
492
+ str = str.replace(/^\s\s*/, '');
493
+ ws = /\s/;
494
+ i = str.length;
495
+ while (ws.test(str.charAt(--i))) {
496
+ continue;
497
+ }
498
+ return str.slice(0, i + 1);
499
+ };
500
+ }