Skip to content

Commit 2e4a180

Browse files
committed
fix(metadata): clean up parser. add prompt false metadata
1 parent 3dfad53 commit 2e4a180

File tree

5 files changed

+130
-25
lines changed

5 files changed

+130
-25
lines changed

src/core/parser.test.ts

+62
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import ava from "ava"
22
import { postprocessMetadata } from "./parser.js"
3+
import { getMetadata } from "./utils.js"
34
import { ProcessType } from "./enum.js"
45
import type { Metadata, ScriptMetadata } from "../types/core.js"
56

@@ -95,3 +96,64 @@ ava("postprocessMetadata - empty input", (t) => {
9596

9697
t.deepEqual(result, { type: ProcessType.Prompt })
9798
})
99+
100+
ava("postprocessMetadata - ignores URLs in comments", (t) => {
101+
const fileContents = `
102+
// Get the API key (https://google.com)
103+
// TODO: Check docs at http://example.com
104+
// Regular metadata: value
105+
`
106+
const result = postprocessMetadata({}, fileContents)
107+
108+
// Should only have type since URLs should be ignored
109+
t.deepEqual(result, { type: ProcessType.Prompt })
110+
})
111+
112+
ava("getMetadata - ignores invalid metadata keys", (t) => {
113+
const fileContents = `
114+
// Get the API key (https://google.com): some value
115+
// TODO: Check docs at http://example.com
116+
// Regular metadata: value
117+
// Name: Test Script
118+
// Description: A test script
119+
// Invalid key with spaces: value
120+
// Invalid/key/with/slashes: value
121+
// Invalid-key-with-hyphens: value
122+
`
123+
const result = getMetadata(fileContents)
124+
125+
// Should only parse valid metadata keys
126+
t.deepEqual(result, {
127+
name: "Test Script",
128+
description: "A test script"
129+
})
130+
})
131+
132+
ava("getMetadata - handles various whitespace patterns", (t) => {
133+
const fileContents = `
134+
//Name:First Value
135+
//Name: Second Value
136+
// Name:Third Value
137+
// Name: Fourth Value
138+
// Name:Fifth Value
139+
// Name: Sixth Value
140+
// Name:Tab Value
141+
// Name: Tabbed Value
142+
`
143+
const result = getMetadata(fileContents)
144+
145+
// Should use the first occurrence of each key and handle all whitespace patterns
146+
t.deepEqual(result, {
147+
name: "First Value"
148+
})
149+
150+
// Test each pattern individually to ensure they all work
151+
t.deepEqual(getMetadata("//Name:Test"), { name: "Test" })
152+
t.deepEqual(getMetadata("//Name: Test"), { name: "Test" })
153+
t.deepEqual(getMetadata("// Name:Test"), { name: "Test" })
154+
t.deepEqual(getMetadata("// Name: Test"), { name: "Test" })
155+
t.deepEqual(getMetadata("// Name:Test"), { name: "Test" })
156+
t.deepEqual(getMetadata("// Name: Test"), { name: "Test" })
157+
t.deepEqual(getMetadata("//\tName:Test"), { name: "Test" })
158+
t.deepEqual(getMetadata("//\tName: Test"), { name: "Test" })
159+
})

src/core/utils.ts

+32-25
Original file line numberDiff line numberDiff line change
@@ -211,13 +211,18 @@ const getMetadataFromComments = (contents: string): Record<string, string> => {
211211
const lines = contents.split('\n')
212212
const metadata = {}
213213
let commentStyle = null
214-
let spaceRegex = null
215214
let inMultilineComment = false
216215
let multilineCommentEnd = null
217216

218-
const setCommentStyle = (style: string) => {
219-
commentStyle = style
220-
spaceRegex = new RegExp(`^${commentStyle} ?[^ ]`)
217+
// Valid metadata key pattern: starts with a letter, can contain letters, numbers, and underscores
218+
const validKeyPattern = /^[a-zA-Z][a-zA-Z0-9_]*$/
219+
// Common prefixes to ignore
220+
const ignoreKeyPrefixes = ['TODO', 'FIXME', 'NOTE', 'HACK', 'XXX', 'BUG']
221+
222+
// Regex to match comment lines with metadata
223+
const commentRegex = {
224+
'//': /^\/\/\s*([^:]+):(.*)$/,
225+
'#': /^#\s*([^:]+):(.*)$/
221226
}
222227

223228
for (const line of lines) {
@@ -249,40 +254,42 @@ const getMetadataFromComments = (contents: string): Record<string, string> => {
249254
// Skip lines that are part of a multiline comment block
250255
if (inMultilineComment) continue
251256

252-
// Determine the comment style based on the first encountered comment line
253-
if (commentStyle === null) {
254-
if (line.startsWith('//') && (line[2] === ' ' || /[a-zA-Z]/.test(line[2]))) {
255-
setCommentStyle('//')
256-
} else if (line.startsWith('#') && (line[1] === ' ' || /[a-zA-Z]/.test(line[1]))) {
257-
setCommentStyle('#')
258-
}
257+
// Determine comment style and try to match metadata
258+
let match = null
259+
if (line.startsWith('//')) {
260+
match = line.match(commentRegex['//'])
261+
commentStyle = '//'
262+
} else if (line.startsWith('#')) {
263+
match = line.match(commentRegex['#'])
264+
commentStyle = '#'
259265
}
260266

261-
// Skip lines that don't start with the determined comment style
262-
if (commentStyle === null || (commentStyle && !line.startsWith(commentStyle))) continue
267+
if (!match) continue
263268

264-
// Check for 0 or 1 space after the comment style
265-
if (!line.match(spaceRegex)) continue
269+
// Extract and trim the key and value
270+
const [, rawKey, value] = match
271+
const trimmedKey = rawKey.trim()
272+
const trimmedValue = value.trim()
266273

267-
// Find the index of the first colon
268-
const colonIndex = line.indexOf(':')
269-
if (colonIndex === -1) continue
274+
// Skip if key starts with common prefixes to ignore
275+
if (ignoreKeyPrefixes.some(prefix => trimmedKey.toUpperCase().startsWith(prefix))) continue
270276

271-
// Extract key and value based on the colon index
272-
let key = line.substring(commentStyle.length, colonIndex).trim()
277+
// Skip if key doesn't match valid pattern
278+
if (!validKeyPattern.test(trimmedKey)) continue
273279

280+
// Transform the key case
281+
let key = trimmedKey
274282
if (key?.length > 0) {
275283
key = key[0].toLowerCase() + key.slice(1)
276284
}
277-
const value = line.substring(colonIndex + 1).trim()
278285

279286
// Skip empty keys or values
280-
if (!key || !value) {
287+
if (!key || !trimmedValue) {
281288
continue
282289
}
283290

284291
let parsedValue: string | boolean | number
285-
let lowerValue = value.toLowerCase()
292+
let lowerValue = trimmedValue.toLowerCase()
286293
let lowerKey = key.toLowerCase()
287294
switch (true) {
288295
case lowerValue === 'true':
@@ -292,10 +299,10 @@ const getMetadataFromComments = (contents: string): Record<string, string> => {
292299
parsedValue = false
293300
break
294301
case lowerKey === 'timeout':
295-
parsedValue = parseInt(value, 10)
302+
parsedValue = Number.parseInt(trimmedValue, 10)
296303
break
297304
default:
298-
parsedValue = value
305+
parsedValue = trimmedValue
299306
}
300307

301308
// Only assign if the key hasn't been assigned before

src/types/core.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,8 @@ export interface Metadata {
542542
index?: number
543543
/** Indicates whether to disable logs for the script */
544544
log?: boolean
545+
/** Optimization: if this script won't require a prompt, set this to false */
546+
prompt?:boolean
545547
}
546548

547549
export interface ProcessInfo {

src/types/kitapp.d.ts

+7
Original file line numberDiff line numberDiff line change
@@ -1566,6 +1566,13 @@ declare global {
15661566
* ```ts
15671567
* await notify("Attention!")
15681568
* ```
1569+
* #### notify example body
1570+
* ```ts
1571+
* await notify({
1572+
* title: "Title text goes here",
1573+
* body: "Body text goes here",
1574+
* });
1575+
* ```
15691576
[Examples](https://scriptkit.com?query=notify) | [Docs](https://johnlindquist.github.io/kit-docs/#notify) | [Discussions](https://github.com/johnlindquist/kit/discussions?discussions_q=notify)
15701577
*/
15711578
var notify: Notify

src/types/pro.d.ts

+27
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,33 @@ declare global {
228228
[Examples](https://scriptkit.com?query=widget) | [Docs](https://johnlindquist.github.io/kit-docs/#widget) | [Discussions](https://github.com/johnlindquist/kit/discussions?discussions_q=widget)
229229
*/
230230
var widget: Widget
231+
/**
232+
* A `vite` generates a vite project and opens it in its own window.
233+
* 1. The first argument is the name of the folder you want generated in ~/.kenv/vite/your-folder
234+
* 2. Optional: the second argument is ["Browser Window Options"](https://www.electronjs.org/docs/latest/api/browser-window#new-browserwindowoptions)
235+
* #### vite example
236+
* ```ts
237+
* const { workArea } = await getActiveScreen();
238+
* // Generates/opens a vite project in ~/.kenv/vite/project-path
239+
* const viteWidget = await vite("project-path", {
240+
* x: workArea.x + 100,
241+
* y: workArea.y + 100,
242+
* width: 640,
243+
* height: 480,
244+
* });
245+
* // In your ~/.kenv/vite/project-path/src/App.tsx (if you picked React)
246+
* // use the "send" api to send messages. "send" is injected on the window object
247+
* // <input type="text" onInput={(e) => send("input", e.target.value)} />
248+
* const filePath = home("vite-example.txt");
249+
* viteWidget.on(
250+
* "input",
251+
* debounce(async (input) => {
252+
* await writeFile(filePath, input);
253+
* }, 1000)
254+
* );
255+
* ```
256+
[Examples](https://scriptkit.com?query=vite) | [Docs](https://johnlindquist.github.io/kit-docs/#vite) | [Discussions](https://github.com/johnlindquist/kit/discussions?discussions_q=vite)
257+
*/
231258
var vite: ViteWidget
232259
/**
233260
* Set the system menu to a custom message/emoji with a list of scripts to run.

0 commit comments

Comments
 (0)