Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
35 changes: 32 additions & 3 deletions src/parser/markdown-to-adf/ASTBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,18 @@ export class ASTBuilder {
// Apply the mark to all child text nodes
return childNodes.map(node => {
if (node.type === 'text') {
const marks = node.marks ? [...node.marks] : [];
let marks = node.marks ? [...node.marks] : [];

// Issue #5: If code mark is being added, remove all other marks
// as it's invalid in ADF to combine code with other marks
if (markType === 'code') {
marks = [];
}
// If code mark already exists, don't add any other marks
else if (marks.some(m => m.type === 'code')) {
return { ...node, marks };
}

marks.push({ type: markType });
return {
...node,
Expand Down Expand Up @@ -625,7 +636,14 @@ export class ASTBuilder {
// Apply the link mark to all child text nodes
return childNodes.map(node => {
if (node.type === 'text') {
const marks = node.marks ? [...node.marks] : [];
let marks = node.marks ? [...node.marks] : [];

// Issue #5: Don't add link mark if code mark exists
// as it's invalid in ADF to combine code with other marks
if (marks.some(m => m.type === 'code')) {
return { ...node, marks };
}

marks.push({ type: 'link', attrs: linkAttrs });
return {
...node,
Expand Down Expand Up @@ -1966,7 +1984,18 @@ export class ASTBuilder {

return childNodes.map(node => {
if (markableNodeTypes.includes(node.type)) {
const marks = node.marks ? [...node.marks] : [];
let marks = node.marks ? [...node.marks] : [];

// Issue #5: If code mark is being added, remove all other marks
// as it's invalid in ADF to combine code with other marks
if (markType === 'code') {
marks = [];
}
// If code mark already exists, don't add any other marks
else if (marks.some(m => m.type === 'code')) {
return { ...node, marks };
}

marks.push(mark);
return {
...node,
Expand Down
15 changes: 0 additions & 15 deletions tests/fixtures/adf/comprehensive-marks.adf
Original file line number Diff line number Diff line change
Expand Up @@ -166,21 +166,6 @@
"type": "text",
"text": "bold italic underlined colored code",
"marks": [
{
"type": "strong"
},
{
"type": "em"
},
{
"type": "underline"
},
{
"type": "textColor",
"attrs": {
"color": "#0066CC"
}
},
{
"type": "code"
}
Expand Down
170 changes: 170 additions & 0 deletions tests/unit/parser/code-mark-exclusivity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/**
* @file Tests for Issue #5: Code mark exclusivity
* @description Validates that code mark removes all other marks and that other marks cannot be added to code
*/

import { describe, it, expect } from '@jest/globals';
import { Parser } from '../../../src/parser/Parser';

describe('Code Mark Exclusivity (Issue #5)', () => {
const parser = new Parser();

describe('Bold + Code combinations', () => {
it('should only have code mark when bold wraps code', () => {
const markdown = '**`code`**';
const result = parser.markdownToAdf(markdown);

const textNode = result.content[0].content[0];
expect(textNode.type).toBe('text');
expect(textNode.text).toBe('code');
expect(textNode.marks).toHaveLength(1);
expect(textNode.marks[0].type).toBe('code');
});

it('should not add bold mark when code mark exists', () => {
const markdown = '`**bold code**`';
const result = parser.markdownToAdf(markdown);

const textNode = result.content[0].content[0];
expect(textNode.type).toBe('text');
expect(textNode.marks).toHaveLength(1);
expect(textNode.marks[0].type).toBe('code');
});
});

describe('Italic + Code combinations', () => {
it('should only have code mark when italic wraps code', () => {
const markdown = '*`code`*';
const result = parser.markdownToAdf(markdown);

const textNode = result.content[0].content[0];
expect(textNode.type).toBe('text');
expect(textNode.text).toBe('code');
expect(textNode.marks).toHaveLength(1);
expect(textNode.marks[0].type).toBe('code');
});

it('should only have code mark when emphasis wraps code', () => {
const markdown = '_`code`_';
const result = parser.markdownToAdf(markdown);

const textNode = result.content[0].content[0];
expect(textNode.type).toBe('text');
expect(textNode.marks).toHaveLength(1);
expect(textNode.marks[0].type).toBe('code');
});
});

describe('Strikethrough + Code combinations', () => {
it('should only have code mark when strikethrough wraps code', () => {
const markdown = '~~`code`~~';
const result = parser.markdownToAdf(markdown);

const textNode = result.content[0].content[0];
expect(textNode.type).toBe('text');
expect(textNode.marks).toHaveLength(1);
expect(textNode.marks[0].type).toBe('code');
});
});

describe('Link + Code combinations', () => {
it('should only have code mark when link wraps code', () => {
const markdown = '[`code`](https://example.com)';
const result = parser.markdownToAdf(markdown);

const textNode = result.content[0].content[0];
expect(textNode.type).toBe('text');
expect(textNode.text).toBe('code');
expect(textNode.marks).toHaveLength(1);
expect(textNode.marks[0].type).toBe('code');
// Link mark should not be added
});
});

describe('Multiple marks + Code combinations', () => {
it('should only have code mark when multiple formatting wraps code', () => {
const markdown = '***`code`***';
const result = parser.markdownToAdf(markdown);

const textNode = result.content[0].content[0];
expect(textNode.type).toBe('text');
expect(textNode.text).toBe('code');
expect(textNode.marks).toHaveLength(1);
expect(textNode.marks[0].type).toBe('code');
});

it('should only have code mark when bold+italic+strike wraps code', () => {
const markdown = '~~***`code`***~~';
const result = parser.markdownToAdf(markdown);

const textNode = result.content[0].content[0];
expect(textNode.type).toBe('text');
expect(textNode.marks).toHaveLength(1);
expect(textNode.marks[0].type).toBe('code');
});
});

describe('Code should stand alone', () => {
it('should preserve only code mark in simple inline code', () => {
const markdown = '`simple code`';
const result = parser.markdownToAdf(markdown);

const textNode = result.content[0].content[0];
expect(textNode.type).toBe('text');
expect(textNode.text).toBe('simple code');
expect(textNode.marks).toHaveLength(1);
expect(textNode.marks[0].type).toBe('code');
});

it('should handle multiple code spans in same paragraph', () => {
const markdown = '**bold** and `code` and *italic*';
const result = parser.markdownToAdf(markdown);

const content = result.content[0].content;

// First should be bold
expect(content[0].marks).toHaveLength(1);
expect(content[0].marks[0].type).toBe('strong');

// Third should be code (only)
expect(content[2].marks).toHaveLength(1);
expect(content[2].marks[0].type).toBe('code');

// Fifth should be italic
expect(content[4].marks).toHaveLength(1);
expect(content[4].marks[0].type).toBe('em');
});
});

describe('Edge cases', () => {
it('should handle code at the start of bold text', () => {
const markdown = '**`code` and bold**';
const result = parser.markdownToAdf(markdown);

const content = result.content[0].content;

// First node should be code only
expect(content[0].marks).toHaveLength(1);
expect(content[0].marks[0].type).toBe('code');

// Second node should be bold
expect(content[1].marks).toHaveLength(1);
expect(content[1].marks[0].type).toBe('strong');
});

it('should handle code at the end of italic text', () => {
const markdown = '*italic and `code`*';
const result = parser.markdownToAdf(markdown);

const content = result.content[0].content;

// First node should be italic
expect(content[0].marks).toHaveLength(1);
expect(content[0].marks[0].type).toBe('em');

// Second node should be code only
expect(content[1].marks).toHaveLength(1);
expect(content[1].marks[0].type).toBe('code');
});
});
});