reelson 0.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/CHANGELOG.md +99 -0
- package/LICENSE +21 -0
- package/NOTICE.md +15 -0
- package/README.md +180 -0
- package/bin/reelson.js +649 -0
- package/bin/reelson.ts +676 -0
- package/bin/run.js +5 -0
- package/demo.config.example.json +51 -0
- package/docs/demo.webp +0 -0
- package/docs/prompting.md +72 -0
- package/docs/style-guide.md +117 -0
- package/examples/demo.config.json +18 -0
- package/examples/todo-add-item/scenario.ts +45 -0
- package/examples/todo-add-item/video.json +35 -0
- package/music/README.md +7 -0
- package/package.json +68 -0
- package/skills/reelson-compose/SKILL.md +226 -0
- package/skills/reelson-compose/schemas/video.schema.json +172 -0
- package/skills/reelson-compose/scripts/build.js +424 -0
- package/skills/reelson-compose/scripts/build.ts +532 -0
- package/skills/reelson-compose/scripts/captions.js +24 -0
- package/skills/reelson-compose/scripts/captions.ts +40 -0
- package/skills/reelson-compose/scripts/check.js +80 -0
- package/skills/reelson-compose/scripts/check.ts +93 -0
- package/skills/reelson-compose/scripts/composition.js +163 -0
- package/skills/reelson-compose/scripts/composition.ts +241 -0
- package/skills/reelson-compose/scripts/hyperframes.js +138 -0
- package/skills/reelson-compose/scripts/hyperframes.ts +157 -0
- package/skills/reelson-compose/scripts/portrait.js +150 -0
- package/skills/reelson-compose/scripts/portrait.ts +218 -0
- package/skills/reelson-compose/scripts/project.js +238 -0
- package/skills/reelson-compose/scripts/project.ts +324 -0
- package/skills/reelson-compose/scripts/studio.js +330 -0
- package/skills/reelson-compose/scripts/studio.ts +394 -0
- package/skills/reelson-compose/scripts/timeline.js +271 -0
- package/skills/reelson-compose/scripts/timeline.ts +514 -0
- package/skills/reelson-compose/scripts/tts.js +303 -0
- package/skills/reelson-compose/scripts/tts.ts +371 -0
- package/skills/reelson-compose/scripts/verify.js +190 -0
- package/skills/reelson-compose/scripts/verify.ts +209 -0
- package/skills/reelson-compose/scripts/voice.js +178 -0
- package/skills/reelson-compose/scripts/voice.ts +224 -0
- package/skills/reelson-compose/scripts/zooms.js +259 -0
- package/skills/reelson-compose/scripts/zooms.ts +333 -0
- package/skills/reelson-compose/sections/README.md +5 -0
- package/skills/reelson-compose/sections/intro/minimal/section.css +27 -0
- package/skills/reelson-compose/sections/intro/minimal/section.html +12 -0
- package/skills/reelson-compose/sections/intro/minimal/section.js +16 -0
- package/skills/reelson-compose/sections/intro/minimal/section.json +9 -0
- package/skills/reelson-compose/sections/intro/poster/section.css +27 -0
- package/skills/reelson-compose/sections/intro/poster/section.html +14 -0
- package/skills/reelson-compose/sections/intro/poster/section.js +28 -0
- package/skills/reelson-compose/sections/intro/poster/section.json +9 -0
- package/skills/reelson-compose/sections/intro/split/section.css +30 -0
- package/skills/reelson-compose/sections/intro/split/section.html +12 -0
- package/skills/reelson-compose/sections/intro/split/section.js +19 -0
- package/skills/reelson-compose/sections/intro/split/section.json +9 -0
- package/skills/reelson-compose/sections/outro/compact/section.css +23 -0
- package/skills/reelson-compose/sections/outro/compact/section.html +10 -0
- package/skills/reelson-compose/sections/outro/compact/section.js +11 -0
- package/skills/reelson-compose/sections/outro/compact/section.json +8 -0
- package/skills/reelson-compose/sections/outro/endcard/section.css +25 -0
- package/skills/reelson-compose/sections/outro/endcard/section.html +13 -0
- package/skills/reelson-compose/sections/outro/endcard/section.js +13 -0
- package/skills/reelson-compose/sections/outro/endcard/section.json +8 -0
- package/skills/reelson-compose/sections/outro/wordmark/section.css +11 -0
- package/skills/reelson-compose/sections/outro/wordmark/section.html +5 -0
- package/skills/reelson-compose/sections/outro/wordmark/section.js +12 -0
- package/skills/reelson-compose/sections/outro/wordmark/section.json +8 -0
- package/skills/reelson-compose/sections/recap/compact/section.css +26 -0
- package/skills/reelson-compose/sections/recap/compact/section.html +8 -0
- package/skills/reelson-compose/sections/recap/compact/section.js +23 -0
- package/skills/reelson-compose/sections/recap/compact/section.json +11 -0
- package/skills/reelson-compose/sections/recap/steps/section.css +36 -0
- package/skills/reelson-compose/sections/recap/steps/section.html +8 -0
- package/skills/reelson-compose/sections/recap/steps/section.js +27 -0
- package/skills/reelson-compose/sections/recap/steps/section.json +11 -0
- package/skills/reelson-compose/studio/index.html +81 -0
- package/skills/reelson-compose/studio/studio.css +142 -0
- package/skills/reelson-compose/studio/studio.js +1031 -0
- package/skills/reelson-compose/templates/README.md +193 -0
- package/skills/reelson-compose/templates/classic/assets/vendor/Inter-OFL.txt +93 -0
- package/skills/reelson-compose/templates/classic/assets/vendor/gsap.min.js +11 -0
- package/skills/reelson-compose/templates/classic/assets/vendor/inter-latin-ext-wght-normal.woff2 +0 -0
- package/skills/reelson-compose/templates/classic/assets/vendor/inter-latin-wght-normal.woff2 +0 -0
- package/skills/reelson-compose/templates/classic/stage.html +447 -0
- package/skills/reelson-compose/templates/classic/template.json +5 -0
- package/skills/reelson-record/SKILL.md +220 -0
- package/skills/reelson-record/schemas/demo.config.schema.json +269 -0
- package/skills/reelson-record/scripts/capture.js +142 -0
- package/skills/reelson-record/scripts/capture.ts +190 -0
- package/skills/reelson-record/scripts/config.js +148 -0
- package/skills/reelson-record/scripts/config.ts +264 -0
- package/skills/reelson-record/scripts/cursor-overlay.js +111 -0
- package/skills/reelson-record/scripts/cursor-overlay.ts +122 -0
- package/skills/reelson-record/scripts/doctor.js +128 -0
- package/skills/reelson-record/scripts/doctor.ts +147 -0
- package/skills/reelson-record/scripts/languages.js +29 -0
- package/skills/reelson-record/scripts/languages.ts +41 -0
- package/skills/reelson-record/scripts/record.js +365 -0
- package/skills/reelson-record/scripts/record.ts +451 -0
- package/skills/reelson-record/scripts/scenario.js +356 -0
- package/skills/reelson-record/scripts/scenario.ts +570 -0
- package/skills/reelson-record/scripts/validate.js +143 -0
- package/skills/reelson-record/scripts/validate.ts +179 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small JSON Schema walker for demo.config.json and video.json — enough of
|
|
3
|
+
* the spec for our two schemas (type, properties, additionalProperties,
|
|
4
|
+
* required, enum, items, minimum/maximum, minItems/maxItems, anyOf), with
|
|
5
|
+
* "did you mean" hints for mistyped keys. Keys starting with `$` ($schema,
|
|
6
|
+
* $comment) are always allowed.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
export function loadSchema(path) {
|
|
10
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
11
|
+
}
|
|
12
|
+
/** Returns human-readable problems; empty when the value matches. */
|
|
13
|
+
export function validate(value, schema, at = '', root = schema) {
|
|
14
|
+
const where = at || '(root)';
|
|
15
|
+
if (schema.$ref) {
|
|
16
|
+
const name = schema.$ref.replace('#/definitions/', '');
|
|
17
|
+
const target = root.definitions?.[name];
|
|
18
|
+
if (!target) {
|
|
19
|
+
throw new Error(`schema: unresolved $ref ${schema.$ref}`);
|
|
20
|
+
}
|
|
21
|
+
return validate(value, target, at, root);
|
|
22
|
+
}
|
|
23
|
+
if (schema.anyOf) {
|
|
24
|
+
const branches = schema.anyOf.map((s) => validate(value, s, at, root));
|
|
25
|
+
if (branches.some((b) => b.length === 0)) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
// Report the branch that got furthest (fewest problems) — usually the intended one.
|
|
29
|
+
return branches.sort((a, b) => a.length - b.length)[0];
|
|
30
|
+
}
|
|
31
|
+
if (schema.type !== undefined) {
|
|
32
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
33
|
+
if (!types.some((t) => matchesType(value, t))) {
|
|
34
|
+
return [`${where}: expected ${types.join(' or ')}, got ${describe(value)}`];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (schema.enum && !schema.enum.some((e) => e === value)) {
|
|
38
|
+
return [`${where}: must be one of ${schema.enum.map((e) => JSON.stringify(e)).join(', ')}`];
|
|
39
|
+
}
|
|
40
|
+
const problems = [];
|
|
41
|
+
if (typeof value === 'number') {
|
|
42
|
+
if (schema.minimum !== undefined && value < schema.minimum) {
|
|
43
|
+
problems.push(`${where}: must be ≥ ${schema.minimum}`);
|
|
44
|
+
}
|
|
45
|
+
if (schema.exclusiveMinimum !== undefined && value <= schema.exclusiveMinimum) {
|
|
46
|
+
problems.push(`${where}: must be > ${schema.exclusiveMinimum}`);
|
|
47
|
+
}
|
|
48
|
+
if (schema.maximum !== undefined && value > schema.maximum) {
|
|
49
|
+
problems.push(`${where}: must be ≤ ${schema.maximum}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
if (schema.minItems !== undefined && value.length < schema.minItems) {
|
|
54
|
+
problems.push(`${where}: needs at least ${schema.minItems} item(s)`);
|
|
55
|
+
}
|
|
56
|
+
if (schema.maxItems !== undefined && value.length > schema.maxItems) {
|
|
57
|
+
problems.push(`${where}: takes at most ${schema.maxItems} item(s)`);
|
|
58
|
+
}
|
|
59
|
+
if (schema.items) {
|
|
60
|
+
value.forEach((item, i) => problems.push(...validate(item, schema.items, `${at}[${i}]`, root)));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (isObject(value)) {
|
|
64
|
+
for (const key of schema.required ?? []) {
|
|
65
|
+
if (!(key in value)) {
|
|
66
|
+
problems.push(`${where}: missing required "${key}"`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const known = Object.keys(schema.properties ?? {});
|
|
70
|
+
for (const [key, child] of Object.entries(value)) {
|
|
71
|
+
if (key.startsWith('$')) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const path = at ? `${at}.${key}` : key;
|
|
75
|
+
const propSchema = schema.properties?.[key];
|
|
76
|
+
if (propSchema) {
|
|
77
|
+
problems.push(...validate(child, propSchema, path, root));
|
|
78
|
+
}
|
|
79
|
+
else if (schema.additionalProperties === false) {
|
|
80
|
+
const hint = closest(key, known);
|
|
81
|
+
problems.push(`${path}: unknown key${hint ? ` — did you mean "${hint}"?` : ''}`);
|
|
82
|
+
}
|
|
83
|
+
else if (isObject(schema.additionalProperties)) {
|
|
84
|
+
problems.push(...validate(child, schema.additionalProperties, path, root));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return problems;
|
|
89
|
+
}
|
|
90
|
+
function matchesType(value, type) {
|
|
91
|
+
switch (type) {
|
|
92
|
+
case 'null':
|
|
93
|
+
return value === null;
|
|
94
|
+
case 'array':
|
|
95
|
+
return Array.isArray(value);
|
|
96
|
+
case 'object':
|
|
97
|
+
return isObject(value);
|
|
98
|
+
case 'integer':
|
|
99
|
+
return Number.isInteger(value);
|
|
100
|
+
case 'number':
|
|
101
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
102
|
+
default:
|
|
103
|
+
return typeof value === type;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function isObject(v) {
|
|
107
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
108
|
+
}
|
|
109
|
+
function describe(v) {
|
|
110
|
+
if (v === null) {
|
|
111
|
+
return 'null';
|
|
112
|
+
}
|
|
113
|
+
if (Array.isArray(v)) {
|
|
114
|
+
return 'array';
|
|
115
|
+
}
|
|
116
|
+
return typeof v === 'string' ? `string ${JSON.stringify(v)}` : typeof v;
|
|
117
|
+
}
|
|
118
|
+
/** The known key within edit distance 3 of `key`, if any. */
|
|
119
|
+
export function closest(key, known) {
|
|
120
|
+
let best = null;
|
|
121
|
+
let bestDistance = 4;
|
|
122
|
+
for (const candidate of known) {
|
|
123
|
+
const d = distance(key.toLowerCase(), candidate.toLowerCase());
|
|
124
|
+
if (d < bestDistance) {
|
|
125
|
+
best = candidate;
|
|
126
|
+
bestDistance = d;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return best;
|
|
130
|
+
}
|
|
131
|
+
function distance(a, b) {
|
|
132
|
+
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
133
|
+
for (let i = 1; i <= a.length; i++) {
|
|
134
|
+
let previous = row[0];
|
|
135
|
+
row[0] = i;
|
|
136
|
+
for (let j = 1; j <= b.length; j++) {
|
|
137
|
+
const current = row[j];
|
|
138
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
139
|
+
previous = current;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return row[b.length];
|
|
143
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small JSON Schema walker for demo.config.json and video.json — enough of
|
|
3
|
+
* the spec for our two schemas (type, properties, additionalProperties,
|
|
4
|
+
* required, enum, items, minimum/maximum, minItems/maxItems, anyOf), with
|
|
5
|
+
* "did you mean" hints for mistyped keys. Keys starting with `$` ($schema,
|
|
6
|
+
* $comment) are always allowed.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from 'node:fs'
|
|
9
|
+
|
|
10
|
+
export interface Schema {
|
|
11
|
+
type?: string | string[]
|
|
12
|
+
properties?: Record<string, Schema>
|
|
13
|
+
additionalProperties?: boolean | Schema
|
|
14
|
+
required?: string[]
|
|
15
|
+
enum?: unknown[]
|
|
16
|
+
items?: Schema
|
|
17
|
+
minimum?: number
|
|
18
|
+
maximum?: number
|
|
19
|
+
exclusiveMinimum?: number
|
|
20
|
+
minItems?: number
|
|
21
|
+
maxItems?: number
|
|
22
|
+
anyOf?: Schema[]
|
|
23
|
+
$ref?: string
|
|
24
|
+
definitions?: Record<string, Schema>
|
|
25
|
+
description?: string
|
|
26
|
+
[key: string]: unknown
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function loadSchema(path: string): Schema {
|
|
30
|
+
return JSON.parse(readFileSync(path, 'utf8')) as Schema
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Returns human-readable problems; empty when the value matches. */
|
|
34
|
+
export function validate(
|
|
35
|
+
value: unknown,
|
|
36
|
+
schema: Schema,
|
|
37
|
+
at = '',
|
|
38
|
+
root: Schema = schema,
|
|
39
|
+
): string[] {
|
|
40
|
+
const where = at || '(root)'
|
|
41
|
+
|
|
42
|
+
if (schema.$ref) {
|
|
43
|
+
const name = schema.$ref.replace('#/definitions/', '')
|
|
44
|
+
const target = root.definitions?.[name]
|
|
45
|
+
if (!target) {
|
|
46
|
+
throw new Error(`schema: unresolved $ref ${schema.$ref}`)
|
|
47
|
+
}
|
|
48
|
+
return validate(value, target, at, root)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (schema.anyOf) {
|
|
52
|
+
const branches = schema.anyOf.map((s) => validate(value, s, at, root))
|
|
53
|
+
if (branches.some((b) => b.length === 0)) {
|
|
54
|
+
return []
|
|
55
|
+
}
|
|
56
|
+
// Report the branch that got furthest (fewest problems) — usually the intended one.
|
|
57
|
+
return branches.sort((a, b) => a.length - b.length)[0]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (schema.type !== undefined) {
|
|
61
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type]
|
|
62
|
+
if (!types.some((t) => matchesType(value, t))) {
|
|
63
|
+
return [`${where}: expected ${types.join(' or ')}, got ${describe(value)}`]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (schema.enum && !schema.enum.some((e) => e === value)) {
|
|
67
|
+
return [`${where}: must be one of ${schema.enum.map((e) => JSON.stringify(e)).join(', ')}`]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const problems: string[] = []
|
|
71
|
+
if (typeof value === 'number') {
|
|
72
|
+
if (schema.minimum !== undefined && value < schema.minimum) {
|
|
73
|
+
problems.push(`${where}: must be ≥ ${schema.minimum}`)
|
|
74
|
+
}
|
|
75
|
+
if (schema.exclusiveMinimum !== undefined && value <= schema.exclusiveMinimum) {
|
|
76
|
+
problems.push(`${where}: must be > ${schema.exclusiveMinimum}`)
|
|
77
|
+
}
|
|
78
|
+
if (schema.maximum !== undefined && value > schema.maximum) {
|
|
79
|
+
problems.push(`${where}: must be ≤ ${schema.maximum}`)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (Array.isArray(value)) {
|
|
84
|
+
if (schema.minItems !== undefined && value.length < schema.minItems) {
|
|
85
|
+
problems.push(`${where}: needs at least ${schema.minItems} item(s)`)
|
|
86
|
+
}
|
|
87
|
+
if (schema.maxItems !== undefined && value.length > schema.maxItems) {
|
|
88
|
+
problems.push(`${where}: takes at most ${schema.maxItems} item(s)`)
|
|
89
|
+
}
|
|
90
|
+
if (schema.items) {
|
|
91
|
+
value.forEach((item, i) => problems.push(...validate(item, schema.items as Schema, `${at}[${i}]`, root)))
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (isObject(value)) {
|
|
96
|
+
for (const key of schema.required ?? []) {
|
|
97
|
+
if (!(key in value)) {
|
|
98
|
+
problems.push(`${where}: missing required "${key}"`)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const known = Object.keys(schema.properties ?? {})
|
|
102
|
+
for (const [key, child] of Object.entries(value)) {
|
|
103
|
+
if (key.startsWith('$')) {
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
const path = at ? `${at}.${key}` : key
|
|
107
|
+
const propSchema = schema.properties?.[key]
|
|
108
|
+
if (propSchema) {
|
|
109
|
+
problems.push(...validate(child, propSchema, path, root))
|
|
110
|
+
} else if (schema.additionalProperties === false) {
|
|
111
|
+
const hint = closest(key, known)
|
|
112
|
+
problems.push(`${path}: unknown key${hint ? ` — did you mean "${hint}"?` : ''}`)
|
|
113
|
+
} else if (isObject(schema.additionalProperties)) {
|
|
114
|
+
problems.push(...validate(child, schema.additionalProperties, path, root))
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return problems
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function matchesType(value: unknown, type: string): boolean {
|
|
123
|
+
switch (type) {
|
|
124
|
+
case 'null':
|
|
125
|
+
return value === null
|
|
126
|
+
case 'array':
|
|
127
|
+
return Array.isArray(value)
|
|
128
|
+
case 'object':
|
|
129
|
+
return isObject(value)
|
|
130
|
+
case 'integer':
|
|
131
|
+
return Number.isInteger(value)
|
|
132
|
+
case 'number':
|
|
133
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
134
|
+
default:
|
|
135
|
+
return typeof value === type
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isObject(v: unknown): v is Record<string, unknown> {
|
|
140
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function describe(v: unknown): string {
|
|
144
|
+
if (v === null) {
|
|
145
|
+
return 'null'
|
|
146
|
+
}
|
|
147
|
+
if (Array.isArray(v)) {
|
|
148
|
+
return 'array'
|
|
149
|
+
}
|
|
150
|
+
return typeof v === 'string' ? `string ${JSON.stringify(v)}` : typeof v
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The known key within edit distance 3 of `key`, if any. */
|
|
154
|
+
export function closest(key: string, known: string[]): string | null {
|
|
155
|
+
let best: string | null = null
|
|
156
|
+
let bestDistance = 4
|
|
157
|
+
for (const candidate of known) {
|
|
158
|
+
const d = distance(key.toLowerCase(), candidate.toLowerCase())
|
|
159
|
+
if (d < bestDistance) {
|
|
160
|
+
best = candidate
|
|
161
|
+
bestDistance = d
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return best
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function distance(a: string, b: string): number {
|
|
168
|
+
const row = Array.from({ length: b.length + 1 }, (_, i) => i)
|
|
169
|
+
for (let i = 1; i <= a.length; i++) {
|
|
170
|
+
let previous = row[0]
|
|
171
|
+
row[0] = i
|
|
172
|
+
for (let j = 1; j <= b.length; j++) {
|
|
173
|
+
const current = row[j]
|
|
174
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1))
|
|
175
|
+
previous = current
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return row[b.length]
|
|
179
|
+
}
|