Skip to content

Commit

Permalink
TS library function for replacing gx markdown labels.
Browse files Browse the repository at this point in the history
  • Loading branch information
jmchilton committed Jan 11, 2024
1 parent 02a6cb6 commit 00cb4f2
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 8 deletions.
104 changes: 103 additions & 1 deletion client/src/components/Markdown/parse.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getArgs } from "./parse";
import { getArgs, replaceLabel, splitMarkdown } from "./parse";

describe("parse.ts", () => {
describe("getArgs", () => {
Expand All @@ -7,4 +7,106 @@ describe("parse.ts", () => {
expect(args.name).toBe("job_metrics");
});
});

describe("splitMarkdown", () => {
it("strip leading whitespace by default", () => {
const { sections } = splitMarkdown("\n```galaxy\njob_metrics(job_id=THISFAKEID)\n```");
expect(sections.length).toBe(1);
});

it("should not strip leading whitespace if disabled", () => {
const { sections } = splitMarkdown("\n```galaxy\njob_metrics(job_id=THISFAKEID)\n```", true);
expect(sections.length).toBe(2);
expect(sections[0].content).toBe("\n");
});
});

describe("replaceLabel", () => {
it("should leave unaffected markdown alone", () => {
const input = "some random\n`markdown content`\n";
const result = replaceLabel(input, "output", "from", "to");
expect(result).toBe(result);
});

it("should leave unaffected galaxy directives alone", () => {
const input = "some random\n`markdown content`\n```galaxy\ncurrent_time()\n```\n";
const result = replaceLabel(input, "output", "from", "to");
expect(result).toBe(result);
});

it("should leave galaxy directives of same type with other labels alone", () => {
const input = "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(output=moo)\n```\n";
const result = replaceLabel(input, "output", "from", "to");
expect(result).toBe(result);
});

it("should leave galaxy directives of other types with same labels alone", () => {
const input = "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(input=from)\n```\n";
const result = replaceLabel(input, "output", "from", "to");
expect(result).toBe(result);
});

it("should swap simple directives of specified type", () => {
const input = "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(output=from)\n```\n";
const output = "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(output=to)\n```\n";
const result = replaceLabel(input, "output", "from", "to");
expect(result).toBe(output);
});

it("should swap single quoted directives of specified type", () => {
const input = "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(output='from')\n```\n";
const output = "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(output=to)\n```\n";
const result = replaceLabel(input, "output", "from", "to");
expect(result).toBe(output);
});

it("should swap single quoted directives of specified type with extra args", () => {
const input =
"some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(footer='cow', output='from', title=dog)\n```\n";
const output =
"some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(footer='cow', output=to, title=dog)\n```\n";
const result = replaceLabel(input, "output", "from", "to");
expect(result).toBe(output);
});

it("should swap double quoted directives of specified type", () => {
const input = 'some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(output="from")\n```\n';
const output = "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(output=to)\n```\n";
const result = replaceLabel(input, "output", "from", "to");
expect(result).toBe(output);
});

it("should swap double quoted directives of specified type with extra args", () => {
const input =
"some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(footer='cow', output=\"from\", title=dog)\n```\n";
const output =
"some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(footer='cow', output=to, title=dog)\n```\n";
const result = replaceLabel(input, "output", "from", "to");
expect(result).toBe(output);
});

it("should leave non-arguments alone", () => {
const input =
"some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(title='cow from farm', output=from)\n```\n";
const output =
"some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(title='cow from farm', output=to)\n```\n";
const result = replaceLabel(input, "output", "from", "to");
expect(result).toBe(output);
});

// not a valid workflow label per se but make sure we're escaping the regex to be safe
it("should not be messed up by labels containing regexp content", () => {
const input = "```galaxy\nhistory_dataset_embedded(output='from(')\n```\n";
const output = "```galaxy\nhistory_dataset_embedded(output=to$1)\n```\n";
const result = replaceLabel(input, "output", "from(", "to$1");
expect(result).toBe(output);
});

it("should not swallow leading newlines", () => {
const input = "\n```galaxy\nhistory_dataset_embedded(output='from')\n```\n";
const output = "\n```galaxy\nhistory_dataset_embedded(output=to)\n```\n";
const result = replaceLabel(input, "output", "from", "to");
expect(result).toBe(output);
});
});
});
70 changes: 63 additions & 7 deletions client/src/components/Markdown/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ const FUNCTION_ARGUMENT_REGEX = `\\s*[\\w\\|]+\\s*=` + FUNCTION_ARGUMENT_VALUE_R
const FUNCTION_CALL_LINE = `\\s*(\\w+)\\s*\\(\\s*(?:(${FUNCTION_ARGUMENT_REGEX})(,${FUNCTION_ARGUMENT_REGEX})*)?\\s*\\)\\s*`;
const FUNCTION_CALL_LINE_TEMPLATE = new RegExp(FUNCTION_CALL_LINE, "m");

export function splitMarkdown(markdown: string) {
const sections = [];
type DefaultSection = { name: "default"; content: string };
type GalaxyDirectiveSection = { name: string; content: string; args: { [key: string]: string } };
type Section = DefaultSection | GalaxyDirectiveSection;

type WorkflowLabelKind = "input" | "output" | "step";

export function splitMarkdown(markdown: string, preserveWhitespace: boolean = false) {
const sections: Section[] = [];
const markdownErrors = [];
let digest = markdown;
while (digest.length > 0) {
Expand All @@ -13,11 +19,12 @@ export function splitMarkdown(markdown: string) {
const galaxyEnd = digest.substr(galaxyStart + 1).indexOf("```");
if (galaxyEnd != -1) {
if (galaxyStart > 0) {
const defaultContent = digest.substr(0, galaxyStart).trim();
if (defaultContent) {
const rawContent = digest.substr(0, galaxyStart);
const defaultContent = rawContent.trim();
if (preserveWhitespace || defaultContent) {
sections.push({
name: "default",
content: defaultContent,
content: preserveWhitespace ? rawContent : defaultContent,
});
}
}
Expand Down Expand Up @@ -48,14 +55,54 @@ export function splitMarkdown(markdown: string) {
return { sections, markdownErrors };
}

export function getArgs(content: string) {
export function replaceLabel(
markdown: string,
labelType: WorkflowLabelKind,
fromLabel: string,
toLabel: string
): string {
const { sections } = splitMarkdown(markdown, true);

function rewriteSection(section: Section) {
if ("args" in section) {
const directiveSection = section as GalaxyDirectiveSection;
const args = directiveSection.args;
if (!(labelType in args)) {
return section;
}
const labelValue = args[labelType];
if (labelValue != fromLabel) {
return section;
}
// we've got a section with a matching label and type...
const newArgs = { ...args };
newArgs[labelType] = toLabel;
const argRexExp = namedArgumentRegex(labelType);
const escapedToLabel = escapeRegExpReplacement(toLabel);
const content = directiveSection.content.replace(argRexExp, `$1${escapedToLabel}`);
return {
name: directiveSection.name,
args: newArgs,
content: content,
};
} else {
return section;
}
}

const rewrittenSections = sections.map(rewriteSection);
const rewrittenMarkdown = rewrittenSections.map((section) => section.content).join("");
return rewrittenMarkdown;
}

export function getArgs(content: string): GalaxyDirectiveSection {
const galaxy_function = FUNCTION_CALL_LINE_TEMPLATE.exec(content);
if (galaxy_function == null) {
throw Error("Failed to parse galaxy directive");
}
type ArgsType = { [key: string]: string };
const args: ArgsType = {};
const function_name = galaxy_function[1];
const function_name = galaxy_function[1] as string;
// we need [... ] to return empty string, if regex doesn't match
const function_arguments = [...content.matchAll(new RegExp(FUNCTION_ARGUMENT_REGEX, "g"))];
for (let i = 0; i < function_arguments.length; i++) {
Expand All @@ -77,3 +124,12 @@ export function getArgs(content: string) {
content: content,
};
}

function namedArgumentRegex(argument: string): RegExp {
return new RegExp(`(\\s*${argument}\\s*=)` + FUNCTION_ARGUMENT_VALUE_REGEX);
}

// https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
function escapeRegExpReplacement(value: string): string {
return value.replace(/\$/g, "$$$$");
}

0 comments on commit 00cb4f2

Please sign in to comment.