@widgetic/creator 0.3.53 → 0.3.55

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.
@@ -98,8 +98,9 @@ declare const WidgetDetails: $$__sveltets_2_IsomorphicComponent<{
98
98
  /** Stacking order when multiple Widget Details panels are open. */ panelZIndex?: number;
99
99
  /** Cascade offset index (0 = centered, 1+ = shifted down-right). */ panelCascadeIndex?: number;
100
100
  /** False when another panel has keyboard/chat focus. */ isFocusedPanel?: boolean;
101
- currentStep?: "create" | "edit" | "embed";
101
+ currentStep?: "embed" | "create" | "edit";
102
102
  beginCompositionHydration?: () => void;
103
+ showPublishedArtifact?: (url: string | null) => void;
103
104
  finishCompositionHydration?: () => void;
104
105
  markCodegenPreviewFresh?: () => void;
105
106
  hasLiveWorkerPreview?: () => boolean;
@@ -164,6 +165,7 @@ declare const WidgetDetails: $$__sveltets_2_IsomorphicComponent<{
164
165
  silent?: boolean;
165
166
  }) => void;
166
167
  }, {
168
+ widgetRename: CustomEvent<any>;
167
169
  fixErrors: CustomEvent<any>;
168
170
  retryRepository: CustomEvent<any>;
169
171
  generateNow: CustomEvent<any>;
@@ -184,12 +186,16 @@ declare const WidgetDetails: $$__sveltets_2_IsomorphicComponent<{
184
186
  } & {
185
187
  [evt: string]: CustomEvent<any>;
186
188
  }, {
189
+ 'version-history': {};
190
+ 'code-generation-models': {};
191
+ 'code-generation-status': {};
187
192
  'code-generation': {};
188
193
  'publish-widget': {};
189
194
  'composition-editor': {};
190
195
  'embed-section': {};
191
196
  }, {
192
197
  beginCompositionHydration: () => void;
198
+ showPublishedArtifact: (url: string | null) => void;
193
199
  finishCompositionHydration: () => void;
194
200
  markCodegenPreviewFresh: () => void;
195
201
  hasLiveWorkerPreview: () => boolean;
@@ -13,3 +13,5 @@ export declare function deriveApiErrorMessage(error: unknown, defaultMessage: st
13
13
  export declare function decodeJWT(token: string): Record<string, unknown> | null;
14
14
  export declare function isTokenExpired(token: string): boolean;
15
15
  export declare function fileToBase64(file: File): Promise<string>;
16
+ /** Convert raster images to WebP at quality 0.95 before prompt/upload. Keep GIF/SVG as-is. */
17
+ export declare function imageFileToWebpFile(file: File, quality?: number): Promise<File>;
@@ -146,6 +146,18 @@ export async function deriveApiErrorMessage(error, defaultMessage) {
146
146
  return toErrorMessage(error, defaultMessage);
147
147
  }
148
148
  // ===================== JWT Helpers =====================
149
+ function base64UrlDecode(base64UrlString) {
150
+ const base64 = base64UrlString.replace(/-/g, '+').replace(/_/g, '/');
151
+ const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
152
+ const binary = atob(padded);
153
+ try {
154
+ const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
155
+ return new TextDecoder().decode(bytes);
156
+ }
157
+ catch {
158
+ return binary;
159
+ }
160
+ }
149
161
  export function decodeJWT(token) {
150
162
  try {
151
163
  const parts = token.split('.');
@@ -153,13 +165,9 @@ export function decodeJWT(token) {
153
165
  throw new Error(`Invalid JWT format: expected 3 parts, got ${parts.length}`);
154
166
  }
155
167
  // Verify header parses
156
- const header = parts[0];
157
- const paddedHeader = header + '='.repeat((4 - (header.length % 4)) % 4);
158
- JSON.parse(atob(paddedHeader));
168
+ JSON.parse(base64UrlDecode(parts[0]));
159
169
  // Decode payload
160
- const payload = parts[1];
161
- const paddedPayload = payload + '='.repeat((4 - (payload.length % 4)) % 4);
162
- const decodedPayloadString = atob(paddedPayload);
170
+ const decodedPayloadString = base64UrlDecode(parts[1]);
163
171
  if (!decodedPayloadString || decodedPayloadString.trim() === '') {
164
172
  throw new Error('Empty payload in JWT');
165
173
  }
@@ -194,4 +202,30 @@ export async function fileToBase64(file) {
194
202
  reader.readAsDataURL(file);
195
203
  });
196
204
  }
205
+ /** Convert raster images to WebP at quality 0.95 before prompt/upload. Keep GIF/SVG as-is. */
206
+ export async function imageFileToWebpFile(file, quality = 0.95) {
207
+ if (!file.type.startsWith('image/'))
208
+ return file;
209
+ if (file.type === 'image/gif' || file.type === 'image/svg+xml' || file.type === 'image/webp') {
210
+ return file;
211
+ }
212
+ if (typeof createImageBitmap !== 'function' || typeof document === 'undefined')
213
+ return file;
214
+ const bitmap = await createImageBitmap(file);
215
+ const canvas = document.createElement('canvas');
216
+ canvas.width = bitmap.width;
217
+ canvas.height = bitmap.height;
218
+ const ctx = canvas.getContext('2d');
219
+ if (!ctx)
220
+ return file;
221
+ ctx.drawImage(bitmap, 0, 0);
222
+ bitmap.close();
223
+ const blob = await new Promise((resolve) => {
224
+ canvas.toBlob((next) => resolve(next), 'image/webp', quality);
225
+ });
226
+ if (!blob)
227
+ return file;
228
+ const name = file.name.replace(/\.[^.]+$/, '') + '.webp';
229
+ return new File([blob], name, { type: 'image/webp', lastModified: Date.now() });
230
+ }
197
231
  // VM Session Helpers removed — VM infrastructure decommissioned
@@ -1,6 +1,6 @@
1
1
  export declare const userSession: import("svelte/store").Writable<{
2
2
  user: any;
3
- jwt: any;
3
+ jwt: string;
4
4
  }>;
5
5
  export declare function setSession(user: any, jwt: any): void;
6
6
  export declare function clearSession(): void;
@@ -1,9 +1,24 @@
1
1
  import { writable } from 'svelte/store';
2
2
  import { syncLocalCacheOwner } from '../localCacheGate.js';
3
- export const userSession = writable({
4
- user: null,
5
- jwt: null
6
- });
3
+ function getInitialSession() {
4
+ if (typeof window === 'undefined') {
5
+ return { user: null, jwt: null };
6
+ }
7
+ const jwt = localStorage.getItem('jwt');
8
+ const userStr = localStorage.getItem('user');
9
+ let user = null;
10
+ try {
11
+ if (userStr)
12
+ user = JSON.parse(userStr);
13
+ }
14
+ catch { }
15
+ return { user, jwt };
16
+ }
17
+ const initialSession = getInitialSession();
18
+ export const userSession = writable(initialSession);
19
+ if (initialSession.user) {
20
+ syncLocalCacheOwner(initialSession.user);
21
+ }
7
22
  export function setSession(user, jwt) {
8
23
  userSession.set({ user, jwt });
9
24
  if (jwt)
@@ -49,7 +49,8 @@ let lastTokenUsed = null;
49
49
  let activeUserId = null;
50
50
  /** Runtime API root for embedded hosts (site PUBLIC_API_URL). Overrides VITE_API_URL. */
51
51
  let runtimeApiUrl = null;
52
- const showLogs = true;
52
+ // Set to true for local websocket debugging
53
+ const showLogs = false;
53
54
  export function setAuthToken(token) {
54
55
  currentAuthToken = token;
55
56
  if (token !== lastTokenUsed && socket) {
@@ -42,7 +42,7 @@ export function syncSessionFromSiteAuth() {
42
42
  : session?.user || { id: 'site-user', email: 'user@widgetic.com', name: 'User' };
43
43
  // setSession → syncLocalCacheOwner when enableLocalCache is on
44
44
  setSession(user, token);
45
- console.log('[CreatorApp] Session synced from site auth');
45
+ // console.log('[CreatorApp] Session synced from site auth');
46
46
  return true;
47
47
  }
48
48
  catch (error) {
@@ -1,15 +1,8 @@
1
1
  /**
2
2
  * Embed code generation utilities for the Embed step.
3
3
  *
4
- * Two formats are supported:
5
- * - `iframe`: classic `<iframe>` pointing at the published widget HTML on the CDN.
6
- * - `web-component`: customer-facing `<widgetic>` host tag. The embed SDK
7
- * scans for this tag and hydrates it. (Autonomous custom elements must
8
- * contain a hyphen, so the SDK does not `define("widgetic")` — it finds
9
- * `<widgetic>` in the DOM and renders into it.)
10
- *
11
- * The SDK script is served from `cdn.widgetic.com/sdks/browser/embed-sdk.js`
12
- * (R2 custom domain on the `widgets` bucket — see AGENTS.md embed architecture).
4
+ * iframe: `<iframe src="cdn…/widget.html">` (no SDK).
5
+ * web-component: embed-sdk.js + one-word `<widgetic composition-id>`.
13
6
  */
14
7
  export type EmbedFormat = 'iframe' | 'web-component';
15
8
  export declare const EMBED_FORMATS: EmbedFormat[];
@@ -23,28 +16,11 @@ export interface EmbedCodeParams {
23
16
  width: number;
24
17
  /** Embed height in pixels. */
25
18
  height: number;
26
- /** CDN base URL for the embed SDK script. Defaults to https://cdn.widgetic.com */
19
+ /** CDN origin for embed-sdk.js. Defaults to https://cdn.widgetic.com */
27
20
  sdkBaseUrl?: string;
28
21
  }
29
- /**
30
- * Build the iframe embed snippet. Backward compatible with the previous
31
- * hardcoded block in `CreatorApp.svelte`.
32
- */
33
22
  export declare function generateIframeEmbedCode({ embedSrc, width, height }: EmbedCodeParams): string;
34
- /**
35
- * Build the Web Component embed snippet using `<widgetic>`.
36
- * Requires a composition id — the custom element loader fetches the
37
- * composition + widget module from the CDN via the embed-sdk batch API.
38
- *
39
- * If no composition is selected we fall back to the iframe format
40
- * (caller should check `compositionId` before calling this).
41
- */
42
23
  export declare function generateWebComponentEmbedCode({ compositionId, width, height, sdkBaseUrl }: EmbedCodeParams): string;
43
- /**
44
- * Pick the right embed code for the requested format, with fallback to
45
- * iframe when the Web Component format is requested but no composition
46
- * is selected (custom element requires `composition-id`).
47
- */
48
24
  export declare function generateEmbedCode(format: EmbedFormat, params: EmbedCodeParams): {
49
25
  code: string;
50
26
  effectiveFormat: EmbedFormat;
@@ -1,54 +1,25 @@
1
1
  /**
2
2
  * Embed code generation utilities for the Embed step.
3
3
  *
4
- * Two formats are supported:
5
- * - `iframe`: classic `<iframe>` pointing at the published widget HTML on the CDN.
6
- * - `web-component`: customer-facing `<widgetic>` host tag. The embed SDK
7
- * scans for this tag and hydrates it. (Autonomous custom elements must
8
- * contain a hyphen, so the SDK does not `define("widgetic")` — it finds
9
- * `<widgetic>` in the DOM and renders into it.)
10
- *
11
- * The SDK script is served from `cdn.widgetic.com/sdks/browser/embed-sdk.js`
12
- * (R2 custom domain on the `widgets` bucket — see AGENTS.md embed architecture).
4
+ * iframe: `<iframe src="cdn…/widget.html">` (no SDK).
5
+ * web-component: embed-sdk.js + one-word `<widgetic composition-id>`.
13
6
  */
14
7
  export const EMBED_FORMATS = ['iframe', 'web-component'];
15
8
  export const EMBED_FORMAT_LABELS = {
16
9
  iframe: 'iframe',
17
10
  'web-component': 'Web Component'
18
11
  };
19
- /**
20
- * Build the iframe embed snippet. Backward compatible with the previous
21
- * hardcoded block in `CreatorApp.svelte`.
22
- */
23
12
  export function generateIframeEmbedCode({ embedSrc, width, height }) {
24
13
  return `<iframe src="${embedSrc}" width="${width}" height="${height}" frameborder="0" allowfullscreen></iframe>`;
25
14
  }
26
- /**
27
- * Build the Web Component embed snippet using `<widgetic>`.
28
- * Requires a composition id — the custom element loader fetches the
29
- * composition + widget module from the CDN via the embed-sdk batch API.
30
- *
31
- * If no composition is selected we fall back to the iframe format
32
- * (caller should check `compositionId` before calling this).
33
- */
34
15
  export function generateWebComponentEmbedCode({ compositionId, width, height, sdkBaseUrl = 'https://cdn.widgetic.com' }) {
35
16
  if (!compositionId) {
36
- // Defensive: caller should have fallen back to iframe already.
37
17
  return '';
38
18
  }
39
- return `<script src="${sdkBaseUrl}/sdks/browser/embed-sdk.js" async></script>
40
- <widgetic
41
- composition-id="${compositionId}"
42
- width="${width}"
43
- height="${height}"
44
- render-mode="custom-element">
45
- </widgetic>`;
19
+ const sdkSrc = `${sdkBaseUrl.replace(/\/+$/, '')}/sdks/browser/embed-sdk.js?v=5`;
20
+ return `<script src="${sdkSrc}"></script>
21
+ <widgetic composition-id="${compositionId}" width="${width}" height="${height}"></widgetic>`;
46
22
  }
47
- /**
48
- * Pick the right embed code for the requested format, with fallback to
49
- * iframe when the Web Component format is requested but no composition
50
- * is selected (custom element requires `composition-id`).
51
- */
52
23
  export function generateEmbedCode(format, params) {
53
24
  if (format === 'web-component') {
54
25
  if (!params.compositionId) {
@@ -0,0 +1,5 @@
1
+ /** Zero out padding-right when this element itself shows a vertical scrollbar. */
2
+ export declare function nativeScrollbarPadding(node: HTMLElement, padPx?: number): {
3
+ update(newPadPx: number): void;
4
+ destroy(): void;
5
+ };
@@ -0,0 +1,27 @@
1
+ /** Zero out padding-right when this element itself shows a vertical scrollbar. */
2
+ export function nativeScrollbarPadding(node, padPx = 8) {
3
+ function check() {
4
+ const hasScrollbar = node.scrollHeight > node.clientHeight + 1;
5
+ node.style.setProperty('padding-right', hasScrollbar ? '0px' : `${padPx}px`, 'important');
6
+ }
7
+ const resizeObserver = new ResizeObserver(check);
8
+ resizeObserver.observe(node);
9
+ const mutationObserver = new MutationObserver(check);
10
+ mutationObserver.observe(node, {
11
+ childList: true,
12
+ subtree: true,
13
+ attributes: true,
14
+ attributeFilter: ['style', 'class'],
15
+ });
16
+ check();
17
+ return {
18
+ update(newPadPx) {
19
+ padPx = newPadPx;
20
+ check();
21
+ },
22
+ destroy() {
23
+ resizeObserver.disconnect();
24
+ mutationObserver.disconnect();
25
+ }
26
+ };
27
+ }
@@ -137,10 +137,10 @@ export function shouldUseTusForChatUpload(file) {
137
137
  */
138
138
  export async function uploadChatFileToWidgeticApi(file, config, options = {}) {
139
139
  if (shouldUseTusForChatUpload(file)) {
140
- console.log('[ChatUpload] TUS upload:', file.name, `${(file.size / 1024 / 1024).toFixed(1)}MB`);
140
+ // console.log('[ChatUpload] TUS upload:', file.name, `${(file.size / 1024 / 1024).toFixed(1)}MB`);
141
141
  return uploadTus(file, config, options);
142
142
  }
143
- console.log('[ChatUpload] Multipart upload:', file.name);
143
+ // console.log('[ChatUpload] Multipart upload:', file.name);
144
144
  options.onProgress?.(50);
145
145
  const result = await uploadMultipart(file, config);
146
146
  options.onProgress?.(100);
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@widgetic/creator",
3
- "version": "0.3.53",
3
+ "version": "0.3.55",
4
4
  "type": "module",
5
5
  "peerDependencies": {
6
6
  "@sveltejs/kit": "^2.20.0",
7
- "svelte": "^5.25.0",
8
7
  "@widgetic/api-sdk": "^1.0.10",
9
- "@widgetic/canvas": "^0.5.4",
10
8
  "@widgetic/cache-layer": "^0.1.2",
9
+ "@widgetic/canvas": "^0.5.4",
11
10
  "@widgetic/chat": "^0.1.4",
12
11
  "@widgetic/design-system": "^0.5.8",
13
12
  "@widgetic/editor": "^4.0.1",
14
- "@widgetic/file-browser": "^0.5.17"
13
+ "@widgetic/file-browser": "^0.5.17",
14
+ "svelte": "^5.25.0"
15
15
  },
16
16
  "svelte": "./dist/index.js",
17
17
  "module": "./dist/index.js",
@@ -60,6 +60,7 @@
60
60
  "test:unit": "vitest"
61
61
  },
62
62
  "devDependencies": {
63
+ "@internationalized/date": "^3.12.4",
63
64
  "@playwright/test": "^1.28.1",
64
65
  "@sveltejs/adapter-auto": "^3.0.0",
65
66
  "@sveltejs/kit": "^2.20.2",
@@ -67,6 +68,13 @@
67
68
  "@tailwindcss/postcss": "^4.1.4",
68
69
  "@tailwindcss/typography": "^0.5.15",
69
70
  "@types/eslint": "^9.6.0",
71
+ "@widgetic/api-sdk": "^1.0.10",
72
+ "@widgetic/cache-layer": "^0.1.2",
73
+ "@widgetic/canvas": "^0.5.4",
74
+ "@widgetic/chat": "^0.1.4",
75
+ "@widgetic/design-system": "^0.5.8",
76
+ "@widgetic/editor": "^4.0.1",
77
+ "@widgetic/file-browser": "^0.5.17",
70
78
  "autoprefixer": "^10.4.20",
71
79
  "eslint": "^9.0.0",
72
80
  "eslint-config-prettier": "^9.1.0",
@@ -88,29 +96,15 @@
88
96
  "typescript-eslint": "^8.20.0",
89
97
  "vite": "^6.2.6",
90
98
  "vite-imagetools": "^7.0.4",
91
- "vitest": "^2.0.0",
92
- "@widgetic/api-sdk": "^1.0.10",
93
- "@widgetic/canvas": "^0.5.4",
94
- "@widgetic/cache-layer": "^0.1.2",
95
- "@widgetic/chat": "^0.1.4",
96
- "@widgetic/design-system": "^0.5.8",
97
- "@widgetic/editor": "^4.0.1",
98
- "@widgetic/file-browser": "^0.5.17"
99
+ "vitest": "^2.0.0"
99
100
  },
100
101
  "dependencies": {
101
- "@codesandbox/sandpack-react": "^2.19.9",
102
- "@emotion/styled": "^11.13.0",
103
102
  "@mdi/js": "^7.4.47",
104
- "@mui/material": "^6.1.1",
105
103
  "@sveltejs/adapter-node": "^5.2.4",
106
104
  "@sveltejs/package": "^2.5.8",
107
- "@xterm/addon-fit": "^0.11.0",
108
- "@xterm/xterm": "^6.0.0",
109
105
  "clsx": "^2.1.1",
110
106
  "dotenv": "^16.4.5",
111
- "fabric": "^6.9.1",
112
107
  "mode-watcher": "^1.1.0",
113
- "sandpack-file-explorer": "^0.0.7",
114
108
  "socket.io-client": "^4.8.3",
115
109
  "tus-js-client": "^4.3.1"
116
110
  },
@@ -1,4 +0,0 @@
1
- export declare function generateAudioFromText(description: string, duration?: any): Promise<any>;
2
- export declare function generateImageFromText(str: string, imgWidth: number, imgHeight: number, genMetaObj: any): Promise<any>;
3
- export declare function generateSuperprompt(str: string): Promise<any>;
4
- export declare function wakeUpImageGenModel(): Promise<any>;
@@ -1,94 +0,0 @@
1
- // local endoints methods(proxy)
2
- // genenerate audio from text using Baseten model
3
- export async function generateAudioFromText(description, duration = 5) {
4
- // return { data: "https://replicate.delivery/pbxt/EKK8MxXO69LfeEhhWsy7FA2ku3KTvxrYFO2WxvLochuk1tUSA/output.wav" };
5
- // basten model id
6
- const model_id = "7qrj60r3"; // audio-gen model deployment id from the service we use to host models
7
- // call the model
8
- const result = await callModel({ prompts: [description], duration: Number(duration), model_host: 'baseten', model_id });
9
- console.log('Audio Gen Model response data:', result);
10
- // if no error return the data
11
- if (!result.error && result.status !== 'error' && result.data) {
12
- let res = result.data.length > 0 ? result.data[0] : result.data;
13
- // add the audio data type
14
- res = 'data:audio/mpeg;base64,' + res;
15
- // return the audio data object
16
- return { data: res };
17
- }
18
- // if error return the error
19
- return result;
20
- }
21
- // generate images from text calling an AI Model
22
- const img_gen_model_id = "1vn88"; // image-gen model deployment id from the service we use to host models
23
- export async function generateImageFromText(str, imgWidth, imgHeight, genMetaObj) {
24
- // create the model input
25
- let input = {
26
- prompt: str,
27
- // image size
28
- width: imgWidth, height: imgHeight,
29
- // other params
30
- ...genMetaObj,
31
- // model id
32
- model_id: img_gen_model_id
33
- };
34
- console.log('Model call input:', input);
35
- // call the model
36
- let response = await callModel(input);
37
- console.log('Image Gen Model response:', response);
38
- // check if no result
39
- if (!response)
40
- response = { error: 'No data generated!' };
41
- // if no error add image header and return the data
42
- if (!response.error && response.data) {
43
- // TODO: check if it's an array of images and use first: response.data[0]
44
- // console.log("Model data response:", response.data.length);
45
- // let res = response.data.length > 0 ? response.data[0] : response.data;
46
- // add the image data type
47
- // res = 'data:image/png;base64,' + res;
48
- let res = 'data:image/png;base64,' + response.data;
49
- // return the image data object
50
- return { data: res };
51
- }
52
- // if error return the error
53
- return response;
54
- }
55
- let super_prompt_model_id = "oc3j2"; // superprompt-gen model deployment id from the service we use to host models
56
- export async function generateSuperprompt(str) {
57
- // call the model
58
- let input = { prompt: str, lcm: true, model_host: 'groq', model_id: super_prompt_model_id };
59
- // console.log('Model call input:', input);
60
- const result = await callModel(input);
61
- // console.log('Model response data:', result);
62
- // if no error modify and return the data
63
- if (!result.error && result.data) {
64
- // TODO: check if it's an array of results then use first: results.data[0]
65
- // console.log("Model data result:", result.data.length);
66
- // let res = result.data.length > 0 ? result.data[0] : result.data;
67
- // read the response data
68
- let res = result.data;
69
- // return the image data object
70
- return { data: res };
71
- }
72
- // if error return the error
73
- return result;
74
- }
75
- export async function wakeUpImageGenModel() {
76
- // call the model
77
- const result = await callModel({ model_id: img_gen_model_id, method: 'wake' });
78
- // if no error return the data
79
- return result;
80
- }
81
- async function callModel(input) {
82
- // read model host service name
83
- let model_host = input.model_host || 'beam';
84
- // make a request to local endpoint that calls the model on server side
85
- const response = await fetch(`/api/call-${model_host}-model`, {
86
- method: 'POST',
87
- headers: { 'Content-Type': 'application/json' },
88
- body: JSON.stringify(input),
89
- });
90
- // read the data from the response
91
- const data = await response.json();
92
- // return response data
93
- return data;
94
- }
File without changes
@@ -1,20 +0,0 @@
1
- String.prototype.capitalize = function () {
2
- return this.charAt(0).toUpperCase() + this.slice(1);
3
- };
4
- String.prototype.rmFirstAtUser = function () {
5
- let newString = this.charAt(0) === "@" ? (this.indexOf(" ") > 0 ? this.slice(this.indexOf(" ") + 1) : "") : this;
6
- return newString;
7
- };
8
- // remove punctuation from the transcript
9
- String.prototype.rmPunctuation = function () {
10
- let newString = this.toLowerCase().replace(/[!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~]/g, '');
11
- return newString;
12
- };
13
- String.prototype.splitByWordCount = function (count) {
14
- var arr = this.split(' ');
15
- var r = [];
16
- while (arr.length) {
17
- r.push(arr.splice(0, count).join(' '));
18
- }
19
- return r;
20
- };