Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
More syntax highlighting!
  • Loading branch information
adamziel committed Nov 28, 2025
commit aa020891937eff6d096c1d27fe88b3c25f1fb79e
Original file line number Diff line number Diff line change
Expand Up @@ -1110,8 +1110,12 @@ export function JSONSchemaEditor({

if (!stringInfo) return false;

const language = detectLanguage(stringInfo.path, stringInfo.stepType);
const unescapedValue = unescapeJsonString(stringInfo.rawValue);
const language = detectLanguage(
stringInfo.path,
stringInfo.stepType,
unescapedValue
);

setStringEditorState({
isOpen: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
export type SupportedLanguage = 'php' | 'sql' | 'shell' | 'text';
export type SupportedLanguage =
| 'php'
| 'sql'
| 'shell'
| 'html'
| 'markdown'
| 'text';

interface LanguageRule {
pathPattern: RegExp;
Expand All @@ -25,17 +31,19 @@ const languageRules: LanguageRule[] = [
*
* @param jsonPath - The JSON path as an array of path segments (e.g., ['steps', '0', 'code'])
* @param stepType - Optional step type discriminator value (e.g., 'runPHP')
* @param content - Optional string content for heuristic detection
* @returns The detected language or 'text' if no specific language is detected
*/
export function detectLanguage(
jsonPath: string[],
stepType?: string
stepType?: string,
content?: string
): SupportedLanguage {
// Build a dot-notation path for matching
const pathString =
'.' +
jsonPath
.map((segment, index) => {
.map((segment) => {
// Check if this segment looks like an array index
if (/^\d+$/.test(segment)) {
return `[${segment}]`;
Expand All @@ -52,24 +60,106 @@ export function detectLanguage(
}
}

// Then, try step-type-based detection for code fields
// Then, try step-type-based detection
// If we know the step type, use it to infer the language
if (stepType) {
const lastSegment = jsonPath[jsonPath.length - 1];
const stepTypeLower = stepType.toLowerCase();

if (stepType === 'runPHP' && lastSegment === 'code') {
// PHP-related steps (runPHP, runPHPWithOptions, etc.)
if (stepTypeLower.includes('php')) {
return 'php';
}
if (
stepType === 'runSQL' &&
(lastSegment === 'sql' || lastSegment === 'source')
) {
// SQL-related steps
if (stepTypeLower.includes('sql')) {
return 'sql';
}
if (stepType === 'wp-cli' && lastSegment === 'command') {
// WP-CLI or shell-related steps
if (
stepTypeLower.includes('wp-cli') ||
stepTypeLower.includes('shell')
) {
return 'shell';
}
}

// Finally, try content-based heuristics
if (content) {
const detected = detectLanguageFromContent(content);
if (detected !== 'text') {
return detected;
}
}

return 'text';
}

/**
* Detect language from string content using simple heuristics
*/
export function detectLanguageFromContent(content: string): SupportedLanguage {
const trimmed = content.trim();

// PHP detection
if (
trimmed.startsWith('<?php') ||
trimmed.startsWith('<?') ||
trimmed.startsWith('#!/usr/bin/env php') ||
// Common PHP patterns
/\$[a-zA-Z_]\w*\s*=/.test(trimmed) || // Variable assignment
/function\s+\w+\s*\(/.test(trimmed) || // Function declaration
/\b(echo|print|require|include|use|namespace|class)\b/.test(trimmed)
) {
return 'php';
}

// SQL detection
if (
/^\s*(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP|TRUNCATE|REPLACE)\b/i.test(
trimmed
) ||
/^\s*--.*\n?\s*(SELECT|INSERT|UPDATE|DELETE|CREATE)/im.test(trimmed) || // SQL with comment
/\b(FROM|WHERE|JOIN|GROUP BY|ORDER BY|HAVING)\b/i.test(trimmed)
) {
return 'sql';
}

// Shell detection
if (
trimmed.startsWith('#!/bin/') ||
trimmed.startsWith('#!/usr/bin/env') ||
/^(wp|npm|yarn|git|curl|wget|chmod|chown|mkdir|rm|cp|mv|ls|cd|echo|export)\s/m.test(
trimmed
) ||
/\|\s*(grep|awk|sed|xargs|sort|uniq)/.test(trimmed)
) {
return 'shell';
}

// HTML detection
if (
/^\s*<!DOCTYPE\s+html/i.test(trimmed) ||
/^\s*<html[\s>]/i.test(trimmed) ||
/<(div|span|p|a|img|table|form|input|button|head|body|script|style|link|meta)[\s>\/]/i.test(
trimmed
)
) {
return 'html';
}

// Markdown detection (simple patterns)
if (
/^#{1,6}\s+\S/.test(trimmed) || // Headers: # Header
/^\s*[-*+]\s+\S/.test(trimmed) || // Unordered lists
/^\s*\d+\.\s+\S/.test(trimmed) || // Ordered lists
/\[.+\]\(.+\)/.test(trimmed) || // Links: [text](url)
/^>\s+\S/.test(trimmed) || // Blockquotes
/```[\s\S]*```/.test(trimmed) || // Code blocks
/\*\*.+\*\*/.test(trimmed) || // Bold
/^\s*\|.+\|/.test(trimmed) // Tables
) {
return 'markdown';
}

return 'text';
}

Expand All @@ -84,6 +174,10 @@ export function getLanguageLabel(language: SupportedLanguage): string {
return 'SQL';
case 'shell':
return 'Shell';
case 'html':
return 'HTML';
case 'markdown':
return 'Markdown';
case 'text':
return 'Plain Text';
}
Expand All @@ -100,6 +194,8 @@ export function getAvailableLanguages(): {
{ value: 'php', label: 'PHP' },
{ value: 'sql', label: 'SQL' },
{ value: 'shell', label: 'Shell' },
{ value: 'html', label: 'HTML' },
{ value: 'markdown', label: 'Markdown' },
{ value: 'text', label: 'Plain Text' },
];
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ async function loadLanguageExtension(
return php();
case 'sql':
return import('@codemirror/lang-sql').then((m) => m.sql());
case 'html':
return import('@codemirror/lang-html').then((m) => m.html());
case 'markdown':
return import('@codemirror/lang-markdown').then((m) =>
m.markdown()
);
case 'shell':
// Use a simple setup for shell - no dedicated extension
return null;
Expand Down