Open cron-expression-validator in Script Kit
I'm quite proud of this one. Super useful when you need to build // Schedule
metadata. Also a nice showcase for using Signals in node apps.
https://github.com/johnlindquist/kit/assets/7313176/29beaabc-1703-4313-aa9b-f963ef5b52e3
// Name: Cron Expression Validator// Description: Validates and helps you build Crontab expressions// Shortcode: cron// Author: @JosXa, loosely based on Ricardo Gonçalves Bassete's versionimport "@johnlindquist/kit"import { computed, effect, signal } from "@preact/signals-core"import cronstrue from "cronstrue"import { markdownTable } from "markdown-table"const FONT_SIZE = "0.8em"const allowedCharsTable = markdownTable([["Character", "Meaning"],["`*`", "any value"],["`,`", "value list separator"],["`-`", "range of values"],["`/`", 'step values (e.g. `*/5 * * * *` for "every 5 minutes")'],],{ align: "l" },)const tableHtml = md(allowedCharsTable).replace('<th align="left">', '<th align="left" style="width: 17%">')const input = signal("* * * * *")const parts = computed(() => input.value.split(" "))const parsedExpression = computed(() => {try {return cronstrue.toString(input.value)} catch (err) {return undefined}})const isValid = computed(() => !!parsedExpression.value)const asciiHint = computed(() => {if (!input.value) {return `| | | | || | | | +----- day of the week (0 - 7) (Sunday = 0 or 7)| | | +------- month (1 - 12)| | +--------- day of the month (1 - 31)| +----------- hour (0 - 23)+------------- minute (0 - 59)`.trim()}const hasSecond = parts.value.length >= 6const names = ["second (0 - 59)","minute (0 - 59)","hour (0 - 23)","day of the month (1 - 31)","month (1 - 12)","day of the week (0 - 7) (Sunday = 0 or 7)",]if (!hasSecond) {names.splice(0, 1)}/*0 1 2 3 4 5 👉 partIdx0 | | | | | +----- day of the week (0 - 7) (Sunday = 0 or 7)1 | | | | +------- month (1 - 12)2 | | | +--------- day of the month (1 - 31)3 | | +----------- hour (0 - 23)4 | +------------- minute (0 - 59)5 +--------------- second (0 - 59)👇lineIdx*/const columns = parts.value.reduce((agg, part, partIdx) => {const prev = agg[partIdx - 1]const startCol = prev ? prev.endCol + 1 : 0const endCol = startCol + part.lengthconst name = names[partIdx]!agg.push({partIdx,startCol,endCol,part,gapToPrevious: Math.max(0, endCol - startCol),name,})return agg},[] as Array<{startCol: numberendCol: numberpart: stringgapToPrevious: numberpartIdx: numbername: string}>,)const lines: string[] = []const maxLen = columns.slice(-1)[0]!.endCol + 5for (let lineIdx = -1; lineIdx < columns.length; lineIdx++) {let line = ""for (const { gapToPrevious, partIdx, name } of columns) {if (lineIdx === -1) {line += "|"line += " ".repeat(gapToPrevious)continue}if (partIdx + lineIdx === columns.length - 1) {line += "+"line = line.padEnd(maxLen, "-")line += ` ${name}`break}line += "|"line += " ".repeat(gapToPrevious)}lines.push(line)}return lines.join("\n")})const asciiHintHtml = computed(() =>`<div style="font-size: ${FONT_SIZE};" class="px-4"><pre>${asciiHint}</pre></div>`.trim(),)const resultMessage = computed(() => {if (!input.value) {return ""}return ("<br>" +(parsedExpression.value? `<h3 class="px-4" style="color: rgba(var(--color-primary), var(--tw-text-opacity))">👉 ${parsedExpression.value}</h3>`: `<h3 class="px-4" style="color: #f65671">❌ The expression "${input.value}" cannot be parsed.</h3>`))})const enter = computed(() => (isValid.value ? "Copy" : ""))const panel = computed(() => `<div>${asciiHintHtml.value}${resultMessage.value}<br><hr>${tableHtml}</div>`)const cleanup: Array<() => void> = []await arg({placeholder: "Type a Crontab expression",input: input.value,className: "p-0",inputClassName: "font-mono",css: `#input {min-width: 250px !important;font-size: ${FONT_SIZE} !important;}`,onInit() {cleanup.push(effect(() => setEnter(enter.value)))cleanup.push(effect(() => setPanel(panel.value)))},onInput(val) {if (!val) {input.value = ""return}let sanitized = val// Replace duplicate spaces.replaceAll(/\s{2,}/g, " ")// Leading whitespace.replaceAll(/^\s+/g, "")const s = sanitized.split(" ")// Ensure maximum of 6 partsif (s.length > 6) {sanitized = s.slice(0, 6).join(" ")}if (val !== sanitized) {setInput(sanitized)}input.value = sanitized},enter: enter.value,alwaysOnTop: true,})cleanup.forEach((fn) => fn())await clipboard.writeText(input.value)