Skip to content

Commit

Permalink
fix(metadata): clean up parser. add prompt false metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
johnlindquist committed Feb 22, 2025
1 parent 3dfad53 commit 2e4a180
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 25 deletions.
62 changes: 62 additions & 0 deletions src/core/parser.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import ava from "ava"
import { postprocessMetadata } from "./parser.js"
import { getMetadata } from "./utils.js"
import { ProcessType } from "./enum.js"
import type { Metadata, ScriptMetadata } from "../types/core.js"

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

t.deepEqual(result, { type: ProcessType.Prompt })
})

ava("postprocessMetadata - ignores URLs in comments", (t) => {
const fileContents = `
// Get the API key (https://google.com)
// TODO: Check docs at http://example.com
// Regular metadata: value
`
const result = postprocessMetadata({}, fileContents)

// Should only have type since URLs should be ignored
t.deepEqual(result, { type: ProcessType.Prompt })
})

ava("getMetadata - ignores invalid metadata keys", (t) => {
const fileContents = `
// Get the API key (https://google.com): some value
// TODO: Check docs at http://example.com
// Regular metadata: value
// Name: Test Script
// Description: A test script
// Invalid key with spaces: value
// Invalid/key/with/slashes: value
// Invalid-key-with-hyphens: value
`
const result = getMetadata(fileContents)

// Should only parse valid metadata keys
t.deepEqual(result, {
name: "Test Script",
description: "A test script"
})
})

ava("getMetadata - handles various whitespace patterns", (t) => {
const fileContents = `
//Name:First Value
//Name: Second Value
// Name:Third Value
// Name: Fourth Value
// Name:Fifth Value
// Name: Sixth Value
// Name:Tab Value
// Name: Tabbed Value
`
const result = getMetadata(fileContents)

// Should use the first occurrence of each key and handle all whitespace patterns
t.deepEqual(result, {
name: "First Value"
})

// Test each pattern individually to ensure they all work
t.deepEqual(getMetadata("//Name:Test"), { name: "Test" })
t.deepEqual(getMetadata("//Name: Test"), { name: "Test" })
t.deepEqual(getMetadata("// Name:Test"), { name: "Test" })
t.deepEqual(getMetadata("// Name: Test"), { name: "Test" })
t.deepEqual(getMetadata("// Name:Test"), { name: "Test" })
t.deepEqual(getMetadata("// Name: Test"), { name: "Test" })
t.deepEqual(getMetadata("//\tName:Test"), { name: "Test" })
t.deepEqual(getMetadata("//\tName: Test"), { name: "Test" })
})
57 changes: 32 additions & 25 deletions src/core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,13 +211,18 @@ const getMetadataFromComments = (contents: string): Record<string, string> => {
const lines = contents.split('\n')
const metadata = {}
let commentStyle = null
let spaceRegex = null
let inMultilineComment = false
let multilineCommentEnd = null

const setCommentStyle = (style: string) => {
commentStyle = style
spaceRegex = new RegExp(`^${commentStyle} ?[^ ]`)
// Valid metadata key pattern: starts with a letter, can contain letters, numbers, and underscores
const validKeyPattern = /^[a-zA-Z][a-zA-Z0-9_]*$/
// Common prefixes to ignore
const ignoreKeyPrefixes = ['TODO', 'FIXME', 'NOTE', 'HACK', 'XXX', 'BUG']

// Regex to match comment lines with metadata
const commentRegex = {
'//': /^\/\/\s*([^:]+):(.*)$/,
'#': /^#\s*([^:]+):(.*)$/
}

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

// Determine the comment style based on the first encountered comment line
if (commentStyle === null) {
if (line.startsWith('//') && (line[2] === ' ' || /[a-zA-Z]/.test(line[2]))) {
setCommentStyle('//')
} else if (line.startsWith('#') && (line[1] === ' ' || /[a-zA-Z]/.test(line[1]))) {
setCommentStyle('#')
}
// Determine comment style and try to match metadata
let match = null
if (line.startsWith('//')) {
match = line.match(commentRegex['//'])
commentStyle = '//'
} else if (line.startsWith('#')) {
match = line.match(commentRegex['#'])
commentStyle = '#'
}

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

// Check for 0 or 1 space after the comment style
if (!line.match(spaceRegex)) continue
// Extract and trim the key and value
const [, rawKey, value] = match
const trimmedKey = rawKey.trim()
const trimmedValue = value.trim()

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

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

// Transform the key case
let key = trimmedKey
if (key?.length > 0) {
key = key[0].toLowerCase() + key.slice(1)
}
const value = line.substring(colonIndex + 1).trim()

// Skip empty keys or values
if (!key || !value) {
if (!key || !trimmedValue) {
continue
}

let parsedValue: string | boolean | number
let lowerValue = value.toLowerCase()
let lowerValue = trimmedValue.toLowerCase()
let lowerKey = key.toLowerCase()
switch (true) {
case lowerValue === 'true':
Expand All @@ -292,10 +299,10 @@ const getMetadataFromComments = (contents: string): Record<string, string> => {
parsedValue = false
break
case lowerKey === 'timeout':
parsedValue = parseInt(value, 10)
parsedValue = Number.parseInt(trimmedValue, 10)
break
default:
parsedValue = value
parsedValue = trimmedValue
}

// Only assign if the key hasn't been assigned before
Expand Down
2 changes: 2 additions & 0 deletions src/types/core.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,8 @@ export interface Metadata {
index?: number
/** Indicates whether to disable logs for the script */
log?: boolean
/** Optimization: if this script won't require a prompt, set this to false */
prompt?:boolean
}

export interface ProcessInfo {
Expand Down
7 changes: 7 additions & 0 deletions src/types/kitapp.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1566,6 +1566,13 @@ declare global {
* ```ts
* await notify("Attention!")
* ```
* #### notify example body
* ```ts
* await notify({
* title: "Title text goes here",
* body: "Body text goes here",
* });
* ```
[Examples](https://scriptkit.com?query=notify) | [Docs](https://johnlindquist.github.io/kit-docs/#notify) | [Discussions](https://github.com/johnlindquist/kit/discussions?discussions_q=notify)
*/
var notify: Notify
Expand Down
27 changes: 27 additions & 0 deletions src/types/pro.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,33 @@ declare global {
[Examples](https://scriptkit.com?query=widget) | [Docs](https://johnlindquist.github.io/kit-docs/#widget) | [Discussions](https://github.com/johnlindquist/kit/discussions?discussions_q=widget)
*/
var widget: Widget
/**
* A `vite` generates a vite project and opens it in its own window.
* 1. The first argument is the name of the folder you want generated in ~/.kenv/vite/your-folder
* 2. Optional: the second argument is ["Browser Window Options"](https://www.electronjs.org/docs/latest/api/browser-window#new-browserwindowoptions)
* #### vite example
* ```ts
* const { workArea } = await getActiveScreen();
* // Generates/opens a vite project in ~/.kenv/vite/project-path
* const viteWidget = await vite("project-path", {
* x: workArea.x + 100,
* y: workArea.y + 100,
* width: 640,
* height: 480,
* });
* // In your ~/.kenv/vite/project-path/src/App.tsx (if you picked React)
* // use the "send" api to send messages. "send" is injected on the window object
* // <input type="text" onInput={(e) => send("input", e.target.value)} />
* const filePath = home("vite-example.txt");
* viteWidget.on(
* "input",
* debounce(async (input) => {
* await writeFile(filePath, input);
* }, 1000)
* );
* ```
[Examples](https://scriptkit.com?query=vite) | [Docs](https://johnlindquist.github.io/kit-docs/#vite) | [Discussions](https://github.com/johnlindquist/kit/discussions?discussions_q=vite)
*/
var vite: ViteWidget
/**
* Set the system menu to a custom message/emoji with a list of scripts to run.
Expand Down

0 comments on commit 2e4a180

Please sign in to comment.