@jscad/core 2.3.4 → 2.3.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jscad/core",
3
- "version": "2.3.4",
3
+ "version": "2.3.8",
4
4
  "description": "Core functionality for JSCAD Applications",
5
5
  "repository": "https://github.com/jscad/OpenJSCAD.org",
6
6
  "main": "src/index.js",
@@ -35,10 +35,11 @@
35
35
  "license": "MIT",
36
36
  "dependencies": {
37
37
  "@jscad/array-utils": "2.1.0",
38
- "@jscad/io": "2.0.5",
39
- "@jscad/io-utils": "2.0.4",
40
- "@jscad/modeling": "2.4.0",
41
- "@jscad/vtree": "2.0.5",
38
+ "@jscad/io": "2.0.9",
39
+ "@jscad/io-utils": "2.0.8",
40
+ "@jscad/modeling": "2.5.3",
41
+ "@jscad/vtree": "2.0.9",
42
+ "json5": "2.2.0",
42
43
  "strip-bom": "4.0.0"
43
44
  },
44
45
  "devDependencies": {
@@ -52,5 +53,5 @@
52
53
  "url": "https://opencollective.com/openjscad",
53
54
  "logo": "https://opencollective.com/openjscad/logo.txt"
54
55
  },
55
- "gitHead": "ec60e9f144d712750731f19dbad75f8ed913a51e"
56
+ "gitHead": "0bfbc72afe89ef8dc4f332acf55858831f173d6e"
56
57
  }
@@ -4,6 +4,7 @@ const path = require('path')
4
4
  const posix = path.posix ? path.posix : path
5
5
 
6
6
  const getFileExtensionFromString = require('../utils/getFileExtensionFromString')
7
+ const { combineParameterDefinitions, getParameterDefinitionsFromSource } = require('../parameters/getParameterDefinitionsFromSource')
7
8
 
8
9
  /* find matching path in inputs
9
10
  * @param {} path
@@ -110,6 +111,11 @@ const makeWebRequire = (filesAndFolders, options) => {
110
111
  _compile: (content, fileName) => {
111
112
  const moduleMakerFunction = new Function('require', 'module', content)
112
113
  moduleMakerFunction(_require.bind(null, entry.fullPath), matchingModule)
114
+
115
+ const paramDefFromSource = content.includes('@jscad-params') ? getParameterDefinitionsFromSource(content, fileName) : []
116
+ const originalFunc = matchingModule.exports.getParameterDefinitions
117
+ // replace getParameterDefinitions in the module, with version taht adds parsed definitions
118
+ matchingModule.exports.getParameterDefinitions = () => combineParameterDefinitions(paramDefFromSource, originalFunc ? originalFunc() || [] : [])
113
119
  // add to core to resolve later references
114
120
  // FIXME coreModules[entry.fullPath] = matchingModule.exports
115
121
  }
@@ -0,0 +1,194 @@
1
+ const JSON5 = require('json5')
2
+
3
+ /* Count leading spaces in a line.
4
+ This helps provide more descriptive comments after the parameter.
5
+
6
+ When comment is foundm the number of spaces can be compared with previous parameter definition.
7
+ When comment line is indented more than parameter(incl. parameter name)
8
+ it is considered as description of previous parameter and not a group definition.
9
+
10
+ */
11
+ const countSpaces = (l) => {
12
+ let count = 0
13
+ for (let i = 0; i < l.length; i++) {
14
+ if (l[i] === ' ') count++
15
+ else if (l[i] === '\t') count += 2
16
+ else break
17
+ }
18
+ return count
19
+ }
20
+
21
+ const getParameterDefinitionsFromSource = (script) => {
22
+ const lines = []
23
+ script.split('\n').forEach((l, i) => {
24
+ const trim = l.trim()
25
+ if (trim) {
26
+ lines.push({ code: trim, line: l, lineNum: i + 1, indent: countSpaces(l) })
27
+ }
28
+ })
29
+
30
+ let i = 0; let lineNum; let code; let prev; let prevIndent
31
+ while (i < lines.length) {
32
+ code = lines[i].code
33
+ i++
34
+ if (code.length > 12 && code.indexOf('@jscad-params') !== -1) break
35
+ }
36
+
37
+ let groupIndex = 1
38
+ const defs = []
39
+
40
+ while (i < lines.length) {
41
+ code = lines[i].code
42
+ lineNum = lines[i].lineNum
43
+ if (code[0] === '}') break
44
+
45
+ const isGroup = code[0] === '/'
46
+ if (isGroup && prev) {
47
+ const isHint = prev.type === 'group' || prevIndent + prev.name.length <= lines[i].indent
48
+ if (isHint) {
49
+ prev.hint = prev.hint ? prev.hint + '\n' : ''
50
+ prev.hint += extractTextFromComment(code, lineNum)
51
+ i++
52
+ continue
53
+ }
54
+ }
55
+
56
+ prevIndent = lines[i].indent
57
+ if (isGroup) {
58
+ // group
59
+ const name = '_group_' + (groupIndex++)
60
+ const def = parseComment(code, lineNum, name)
61
+ let caption = def.caption
62
+ if (caption[0] === '>') {
63
+ caption = caption.substring(1).trim()
64
+ if (!def.options) def.options = {}
65
+ def.options.initial = 'closed'
66
+ }
67
+
68
+ defs.push(prev = { name, type: 'group', caption, ...def.options })
69
+ } else {
70
+ const idx = code.indexOf('/')
71
+ if (idx === -1) {
72
+ // also handle case when closing bracket is in same line as last parameter
73
+ // width=11}
74
+ // it is not an exhaustive check but covers aditional case to simplify it for users
75
+ const bracketIdx = code.indexOf('}')
76
+ if (bracketIdx !== -1) code = code.substring(0, bracketIdx)
77
+
78
+ const def = parseDef(code, lineNum)
79
+ def.caption = def.name
80
+ defs.push(prev = def)
81
+
82
+ if (bracketIdx !== -1) break
83
+ } else {
84
+ defs.push(prev = parseOne(
85
+ code.substring(idx).trim(),
86
+ code.substring(0, idx).trim(),
87
+ lineNum, lineNum
88
+ ))
89
+ }
90
+ }
91
+ i++
92
+ }
93
+
94
+ return defs
95
+ }
96
+
97
+ const parseOne = (comment, code, line1, line2) => {
98
+ let def = parseDef(code, line2)
99
+ const { caption, options } = parseComment(comment, line1, def.name)
100
+ def.caption = caption || def.name
101
+ if (options) {
102
+ def = { ...def, ...options }
103
+ if (def.type === 'checkbox' && 'initial' in def) def.checked = true
104
+ if (def.type === 'slider') {
105
+ if (def.min === undefined) {
106
+ def.min = 0
107
+ }
108
+ if (def.max === undefined) {
109
+ def.max = 100
110
+ }
111
+ }
112
+ }
113
+
114
+ return def
115
+ }
116
+
117
+ const extractTextFromComment = (c, lineNum) => {
118
+ const prefix = c.substring(0, 2)
119
+ // after cutting-out the comment marker, there could be more spaces to trim
120
+ if (prefix === '//') c = c.substring(2).trim()
121
+ if (prefix === '/*') {
122
+ if (c.substring(c.length - 2) !== '*/') throw new EvalError(`Multi-line comments not supported in parsed parameter definitions, line:${lineNum}`, 'code', lineNum)
123
+ c = c.substring(2, c.length - 2).trim()
124
+ }
125
+
126
+ return c
127
+ }
128
+
129
+ const parseComment = (comment, lineNum, paramName) => {
130
+ comment = extractTextFromComment(comment, lineNum)
131
+
132
+ const ret = {}
133
+ const idx = comment.indexOf('{')
134
+ if (idx !== -1) {
135
+ try {
136
+ ret.options = JSON5.parse(comment.substring(idx))
137
+ } catch (e) {
138
+ throw new EvalError(`${e.message}, parameter:${paramName}, line:${lineNum}: ${comment.substring(idx)}`, 'code', lineNum)
139
+ }
140
+ comment = comment.substring(0, idx).trim()
141
+ }
142
+
143
+ ret.caption = comment
144
+
145
+ return ret
146
+ }
147
+
148
+ const parseDef = (code, line) => {
149
+ if (code[code.length - 1] === ',') code = code.substring(0, code.length - 1).trim()
150
+ let idx = code.indexOf('=')
151
+
152
+ if (idx === -1) idx = code.indexOf(':')
153
+
154
+ if (idx === -1) {
155
+ return { name: code, type: 'text' }
156
+ } else {
157
+ const initial = code.substring(idx + 1).trim()
158
+
159
+ const ret = { type: 'text', name: code.substring(0, idx).trim() }
160
+
161
+ if (initial === 'true' || initial === 'false') {
162
+ ret.type = 'checkbox'
163
+ ret.checked = initial === 'true'
164
+ } else if (/^[0-9]+$/.test(initial)) {
165
+ ret.type = 'int'
166
+ ret.initial = parseFloat(initial)
167
+ } else if (/^[0-9]+\.[0-9]+$/.test(initial)) {
168
+ ret.type = 'number'
169
+ ret.initial = parseFloat(initial)
170
+ } else {
171
+ try {
172
+ ret.initial = JSON5.parse(initial)
173
+ } catch (e) {
174
+ throw new EvalError(`Error in the initial value definition for ${code} ${e.message}, line:${line}`, 'code', line)
175
+ }
176
+ }
177
+
178
+ return ret
179
+ }
180
+ }
181
+
182
+ const combineParameterDefinitions = (paramDefFromSource, extraDef) => {
183
+ const def = [...paramDefFromSource]
184
+ if (extraDef) {
185
+ extraDef.forEach((param) => {
186
+ const idx = def.findIndex((p) => p.name === param.name)
187
+ if (idx !== -1) def[idx] = param
188
+ else def.push(param)
189
+ })
190
+ }
191
+ return def
192
+ }
193
+
194
+ module.exports = { getParameterDefinitionsFromSource, parseOne, parseComment, parseDef, combineParameterDefinitions }
@@ -0,0 +1,228 @@
1
+ 'use strict'
2
+ const test = require('ava')
3
+
4
+ const { getParameterDefinitionsFromSource, parseOne, parseComment, parseDef } = require('./getParameterDefinitionsFromSource.js')
5
+
6
+ const sampleParams = [
7
+ { name: 'width', caption: 'Width', type: 'int', initial: 145 },
8
+ { name: 'height', caption: 'height', type: 'int', initial: 100 },
9
+ { name: 'division', caption: 'Number of rows (== columns)', type: 'int', initial: 4 },
10
+ { name: 'bottomPieceHeight', caption: 'Water space(mm)', type: 'int', initial: 20 },
11
+
12
+ { name: 'group1', caption: 'Wall thickness', type: 'group' },
13
+ { name: 'thickOut', caption: 'Vertical outside', type: 'number', initial: 0.8 },
14
+ { name: 'thickIn', caption: 'Vertical inside', type: 'number', initial: 0.8 },
15
+ { name: 'thickBottom', caption: 'Bottom', type: 'number', initial: 0.8 }
16
+ ]
17
+
18
+ const sampleScript = `function main({// @jscad-params
19
+ width=145, // Width
20
+ height=100,
21
+ division=4,// Number of rows (== columns)
22
+ bottomPieceHeight=20,// Water space(mm)
23
+
24
+ /* Wall thickness {name:'group1'} */
25
+ thickOut=0.8, //Vertical outside
26
+
27
+ thickIn=0.8,// Vertical inside
28
+ thickBottom=0.8,//Bottom
29
+ }){`
30
+
31
+ const sampleScript2 = `function main({// @jscad-params
32
+ width=145, // Width
33
+ height=100, // height
34
+ division=4, // Number of rows (== columns)
35
+ bottomPieceHeight=20, // Water space(mm)
36
+
37
+ // Wall thickness {name:'group1'}
38
+ thickOut=0.8, // Vertical outside
39
+ thickIn=0.8, // Vertical inside
40
+ thickBottom=0.8, // Bottom
41
+ })`
42
+
43
+ const inputTest = `const jscad = require('@jscad...');
44
+
45
+ var x = 1
46
+
47
+ ${sampleScript}
48
+ var a = 1
49
+
50
+ return circle()
51
+ }
52
+ `
53
+
54
+ test('multiple params', (t) => {
55
+ t.deepEqual(getParameterDefinitionsFromSource(inputTest), sampleParams)
56
+ })
57
+
58
+ test('CURRENT multiple params', (t) => {
59
+ t.deepEqual(getParameterDefinitionsFromSource(sampleScript2), sampleParams)
60
+ })
61
+
62
+ test('line comment', (t) => {
63
+ t.deepEqual(parseComment('// Width '), { caption: 'Width' })
64
+ })
65
+
66
+ test('block comment', (t) => {
67
+ t.deepEqual(parseComment('/* Width */'), { caption: 'Width' })
68
+ })
69
+
70
+ test('comment with data', (t) => {
71
+ t.deepEqual(parseComment('/* Password {type:"password"}*/'), { caption: 'Password', options: { type: 'password' } })
72
+ })
73
+
74
+ test('param without init value', (t) => {
75
+ t.deepEqual(parseDef('width'), { name: 'width', type: 'text' })
76
+ })
77
+
78
+ test('param int', (t) => {
79
+ t.deepEqual(parseDef('age=1,'), { name: 'age', type: 'int', initial: 1 })
80
+ })
81
+
82
+ test('param float', (t) => {
83
+ t.deepEqual(parseDef('angle=1.0,'), { name: 'angle', type: 'number', initial: 1 })
84
+ })
85
+
86
+ test('proeprty float', (t) => {
87
+ t.deepEqual(parseDef('angle:1.0,'), { name: 'angle', type: 'number', initial: 1 })
88
+ })
89
+
90
+ test('param checkbox', (t) => {
91
+ t.deepEqual(parseDef('bigorsmall=true,'), { name: 'bigorsmall', type: 'checkbox', checked: true })
92
+ t.deepEqual(parseDef('bigorsmall=false,'), { name: 'bigorsmall', type: 'checkbox', checked: false })
93
+ })
94
+
95
+ test('param text', (t) => {
96
+ t.deepEqual(parseDef('name,'), { name: 'name', type: 'text' })
97
+ })
98
+
99
+ const testBothDir = function (t, line1, line2, def) {
100
+ t.deepEqual(parseOne(line1, line2), def)
101
+ }
102
+
103
+ test('param checkbox with initial value', (t) => {
104
+ testBothDir(t,
105
+ '// Big? {type:"checkbox"}',
106
+ 'bigorsmall=20,',
107
+ { name: 'bigorsmall', type: 'checkbox', checked: true, initial: 20, caption: 'Big?' }
108
+ )
109
+ })
110
+
111
+ test('param color', (t) => {
112
+ testBothDir(t,
113
+ '// Color? {type:"color"}',
114
+ 'color=\'#FFB431\'',
115
+ { name: 'color', type: 'color', initial: '#FFB431', caption: 'Color?' }
116
+ )
117
+ })
118
+
119
+ test('param date', (t) => {
120
+ testBothDir(t,
121
+ '// Birthday? {type:"date"}',
122
+ 'birthday',
123
+ { name: 'birthday', type: 'date', caption: 'Birthday?' }
124
+ )
125
+ })
126
+
127
+ test('param email', (t) => {
128
+ testBothDir(t,
129
+ '// Email Address? {type:"email"}',
130
+ 'address',
131
+ { name: 'address', type: 'email', caption: 'Email Address?' }
132
+ )
133
+ })
134
+
135
+ test('param password', (t) => {
136
+ testBothDir(t,
137
+ '// Secret? {type:"password"}',
138
+ 'password',
139
+ { name: 'password', type: 'password', caption: 'Secret?' }
140
+ )
141
+ })
142
+
143
+ test('param slider', (t) => {
144
+ testBothDir(t,
145
+ '// How many? {type:"slider", min:2, max:10}',
146
+ 'count',
147
+ { name: 'count', type: 'slider', min: 2, max: 10, caption: 'How many?' }
148
+ )
149
+ })
150
+
151
+ test('param slider2', (t) => {
152
+ testBothDir(t,
153
+ '// How many? {type:"slider", min:2, max:10, step:2}',
154
+ 'count',
155
+ { name: 'count', type: 'slider', min: 2, max: 10, step: 2, caption: 'How many?' }
156
+ )
157
+ })
158
+
159
+ test('param url', (t) => {
160
+ testBothDir(t,
161
+ '// Web page URL? {type:"url"}',
162
+ 'webpage',
163
+ { name: 'webpage', type: 'url', caption: 'Web page URL?' }
164
+ )
165
+ })
166
+
167
+ test('param choice', (t) => {
168
+ testBothDir(t,
169
+ '// Rounded edges {type:\'choice\', values: [0, 1], captions: [\'No\', \'Yes (slow!)\']}',
170
+ 'rounded=0',
171
+ { name: 'rounded', type: 'choice', caption: 'Rounded edges', values: [0, 1], captions: ['No', 'Yes (slow!)'], initial: 0 }
172
+ )
173
+ })
174
+
175
+ const sampleParamsWithHints = [
176
+ { name: 'width', caption: 'Width', type: 'int', initial: 145, hint: 'Width of the complete model\nIncluding other stuff' },
177
+ { name: 'height', caption: 'height', type: 'int', initial: 100, hint: 'Height of the complete model' },
178
+
179
+ { name: '_group_1', caption: 'Wall thickness :group1', type: 'group', hint: 'Extra description of the group so group name can stay short', initial: 'closed' },
180
+ { name: 'thickOut', caption: 'Vertical outside', type: 'number', initial: 0.8 }
181
+ ]
182
+
183
+ const sampleScriptHints = `function main({// @jscad-params
184
+ width=145, // Width
185
+ // Width of the complete model
186
+ // Including other stuff
187
+
188
+ height=100, // height
189
+ // Height of the complete model
190
+
191
+ // > Wall thickness :group1
192
+ // Extra description of the group so group name can stay short
193
+ thickOut=0.8, // Vertical outside
194
+ })`
195
+
196
+ const inputTestHints = `const jscad = require('@jscad...');
197
+
198
+ var x = 1
199
+
200
+ ${sampleScriptHints}
201
+ var a = 1
202
+
203
+ return circle()
204
+ }
205
+ `
206
+
207
+ test('extra hints', (t) => {
208
+ t.deepEqual(getParameterDefinitionsFromSource(inputTestHints), sampleParamsWithHints)
209
+ })
210
+
211
+ const sampleParams2 = [{ name: 'width', caption: 'width', type: 'int', initial: 14 }]
212
+
213
+ test('last param brackets', (t) => {
214
+ t.deepEqual(getParameterDefinitionsFromSource(`function main({//@jscad-params
215
+ width=14})`), sampleParams2)
216
+ })
217
+
218
+ test('multiline error', (t) => {
219
+ t.throws(() => getParameterDefinitionsFromSource(`function main({//@jscad-params
220
+ /*
221
+ width=14})`), { instanceOf: EvalError, message: 'Multi-line comments not supported in parsed parameter definitions, line:2' })
222
+ })
223
+
224
+ test('single line /* ...*/ ok', (t) => {
225
+ t.deepEqual(getParameterDefinitionsFromSource(`function main({//@jscad-params
226
+ /* group */
227
+ width=14})`), [{type:'group', caption:'group', name:'_group_1'},...sampleParams2])
228
+ })