@tosin2013/kanbn
Version:
A CLI Kanban board with AI-powered task management features
261 lines (237 loc) • 7.56 kB
JavaScript
/**
* Markdown parser compatibility layer
*
* This module provides a consistent API for markdown parsing,
* allowing us to switch between different markdown parsers.
*
* Currently implements markdown-it as a replacement for marked.
*/
const MarkdownIt = require('markdown-it');
// Create a default instance with GFM-like features
const md = new MarkdownIt({
html: false, // Disable HTML tags by default (safer)
breaks: true, // Convert \n to <br>
linkify: true, // Autoconvert URL-like text to links
typographer: true // Enable smartquotes and other typographic replacements
});
// Add GFM task lists support with better compatibility with marked
md.use(require('markdown-it-task-lists'), {
enabled: true,
label: true,
labelAfter: false
});
/**
* Parse markdown to HTML
* @param {string} src - Markdown source
* @param {object} options - Options (optional)
* @returns {string} - HTML output
*/
function parse(src, options = {}) {
return md.render(src);
}
/**
* Parse inline markdown to HTML
* @param {string} src - Markdown source
* @param {object} options - Options (optional)
* @returns {string} - HTML output
*/
function parseInline(src, options = {}) {
return md.renderInline(src);
}
/**
* Lexical analysis of markdown (tokenization)
* @param {string} src - Markdown source
* @param {object} options - Options (optional)
* @returns {Array} - Array of tokens
*/
function lexer(src, options = {}) {
// Handle empty or whitespace-only input
if (!src || src.trim() === '') {
return [];
}
try {
// This is a simplified version that returns tokens in a format
// similar to marked's lexer, but with markdown-it's structure
const tokens = md.parse(src, {});
// Process tokens to make them more compatible with marked's format
return processTokens(tokens);
} catch (error) {
console.warn(`Warning: Error in markdown lexer: ${error.message}`);
// Return an empty array on error
return [];
}
}
/**
* Process markdown-it tokens to make them more compatible with marked's format
* @param {Array} tokens - markdown-it tokens
* @returns {Array} - Processed tokens
*/
function processTokens(tokens) {
// Handle empty tokens array
if (!tokens || !Array.isArray(tokens) || tokens.length === 0) {
return [];
}
const result = [];
let currentList = null;
try {
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (!token) continue;
// Handle lists specially to match marked's format
if (token.type === 'bullet_list_open') {
currentList = {
type: 'list',
raw: '',
ordered: false,
start: '',
loose: false,
items: []
};
result.push(currentList);
}
else if (token.type === 'ordered_list_open') {
currentList = {
type: 'list',
raw: '',
ordered: true,
start: token.attrs && token.attrs.start ? token.attrs.start : 1,
loose: false,
items: []
};
result.push(currentList);
}
else if (token.type === 'list_item_open') {
const item = {
type: 'list_item',
raw: '',
task: false,
checked: false,
loose: false,
text: '',
tokens: [{
type: 'text',
raw: '',
text: '',
tokens: [{
type: 'text',
raw: '',
text: ''
}]
}]
};
// Check if this is a task item
if (token.classes && token.classes.includes('task-list-item')) {
item.task = true;
// Find the checkbox input to determine if it's checked
for (let j = i + 1; j < tokens.length && tokens[j].type !== 'list_item_close'; j++) {
if (tokens[j].type === 'inline' && tokens[j].children) {
for (const child of tokens[j].children) {
if (child.type === 'checkbox' && child.attrs && child.attrs.checked) {
item.checked = true;
break;
}
}
}
}
}
// Fallback to the old method for compatibility
else if (i + 2 < tokens.length &&
tokens[i + 1].type === 'paragraph_open' &&
tokens[i + 2].type === 'inline') {
if (tokens[i + 2].content.startsWith('[ ] ')) {
item.task = true;
item.checked = false;
tokens[i + 2].content = tokens[i + 2].content.substring(4);
}
else if (tokens[i + 2].content.startsWith('[x] ')) {
item.task = true;
item.checked = true;
tokens[i + 2].content = tokens[i + 2].content.substring(4);
}
}
if (currentList) {
currentList.items.push(item);
}
}
else if (token.type === 'inline' &&
i > 0 &&
tokens[i - 1] && tokens[i - 1].type === 'paragraph_open' &&
currentList &&
currentList.items && currentList.items.length > 0) {
try {
// Add content to the current list item
const currentItem = currentList.items[currentList.items.length - 1];
currentItem.text = token.content || '';
// Create a safe tokens structure
currentItem.tokens = [{
type: 'text',
raw: token.content || '',
text: token.content || '',
tokens: [{
type: 'text',
raw: token.content || '',
text: token.content || ''
}]
}];
} catch (error) {
console.warn(`Warning: Error processing inline token: ${error.message}`);
}
}
else if (token.type === 'heading_open') {
try {
const level = parseInt(token.tag.substring(1));
const inlineToken = i + 1 < tokens.length ? tokens[i + 1] : null;
if (inlineToken && inlineToken.type === 'inline') {
result.push({
type: 'heading',
raw: `${'#'.repeat(level)} ${inlineToken.content || ''}`,
depth: level,
text: inlineToken.content || '',
tokens: [{
type: 'text',
raw: inlineToken.content || '',
text: inlineToken.content || ''
}]
});
// Skip the next two tokens (inline and heading_close)
i += 2;
}
} catch (error) {
console.warn(`Warning: Error processing heading token: ${error.message}`);
}
}
else if (token.type === 'paragraph_open' &&
!(i > 0 && tokens[i - 1] && tokens[i - 1].type === 'list_item_open')) {
try {
const inlineToken = i + 1 < tokens.length ? tokens[i + 1] : null;
if (inlineToken && inlineToken.type === 'inline') {
result.push({
type: 'paragraph',
raw: inlineToken.content || '',
text: inlineToken.content || '',
tokens: [{
type: 'text',
raw: inlineToken.content || '',
text: inlineToken.content || ''
}]
});
// Skip the next two tokens (inline and paragraph_close)
i += 2;
}
} catch (error) {
console.warn(`Warning: Error processing paragraph token: ${error.message}`);
}
}
}
} catch (error) {
console.warn(`Warning: Error in processTokens: ${error.message}`);
}
return result;
}
module.exports = {
parse,
parseInline,
lexer,
// Expose the markdown-it instance for direct access if needed
md
};