// ラムダノート用 markdownlint カスタムルール
// 使用方法: markdownlint -r ./lambda-note-rules.js your-document.md
module.exports = [
// ルール1: 最上位見出しはレベル2(##)のみ許可
{
names: ["lambda-note-top-level-heading"],
description: "最上位見出しはレベル2(##)を使用してください",
tags: ["headings"],
function: function LN001(params, onError) {
const { tokens } = params;
let hasLevel2Heading = false;
tokens.forEach(token => {
if (token.type === "heading_open") {
const level = parseInt(token.tag.substring(1));
if (level === 1) {
onError({
lineNumber: token.lineNumber,
detail: "最上位見出しにはレベル2(##)を使用してください。レベル1(#)は使用できません。"
});
} else if (level === 2) {
hasLevel2Heading = true;
} else if (level > 3) {
onError({
lineNumber: token.lineNumber,
detail: "見出しは3レベルまでしか使用できません。構造を簡素化してください。"
});
}
}
});
}
},
// ルール2: 段落内での改行推奨(句点ごとの改行)
{
names: ["lambda-note-paragraph-breaks"],
description: "段落内では句点ごとに改行することを推奨します",
tags: ["paragraphs"],
function: function LN002(params, onError) {
const { lines } = params;
lines.forEach((line, index) => {
const trimmedLine = line.trim();
if (trimmedLine && !trimmedLine.startsWith('#') && !trimmedLine.startsWith('*') &&
!trimmedLine.startsWith('1.') && !trimmedLine.startsWith('>') &&
!trimmedLine.startsWith('```') && !trimmedLine.startsWith('|')) {
// 句点が含まれる行で、句点の後に文章が続く場合
const periods = trimmedLine.match(/。[^)」』]*[ぁ-んァ-ヶー一-龯]/g);
if (periods && periods.length > 0) {
onError({
lineNumber: index + 1,
detail: "句点(。)の後で改行することを推奨します。差分が見やすくなります。"
});
}
}
});
}
},
// ルール3: 全角句読点の使用チェック
{
names: ["lambda-note-punctuation"],
description: "句読点は全角(。、)を使用してください",
tags: ["punctuation"],
function: function LN003(params, onError) {
const { lines } = params;
lines.forEach((line, index) => {
// 半角の句読点をチェック(コードブロック内は除外)
if (!line.includes('```') && !line.trim().startsWith('```')) {
if (line.includes('.') && /[ぁ-んァ-ヶー一-龯]\./.test(line)) {
onError({
lineNumber: index + 1,
detail: "句点には全角(。)を使用してください。半角の . は句読点として使用できません。"
});
}
if (line.includes(',') && /[ぁ-んァ-ヶー一-龯],/.test(line)) {
onError({
lineNumber: index + 1,
detail: "読点には全角(、)を使用してください。半角の , は句読点として使用できません。"
});
}
}
});
}
},
// ルール4: コードブロックのキャプションとID必須
{
names: ["lambda-note-code-blocks"],
description: "コードブロックにはIDとキャプションを指定してください",
tags: ["code"],
function: function LN004(params, onError) {
const { tokens } = params;
tokens.forEach(token => {
if (token.type === "fence" && token.info) {
const info = token.info.trim();
if (info && !info.includes('{#lst:') && !info.includes('caption=')) {
onError({
lineNumber: token.lineNumber,
detail: 'コードブロックには {#lst:id language caption="説明"} の形式でIDとキャプションを指定してください。'
});
}
}
});
}
},
// ルール5: 図のキャプションとID必須
{
names: ["lambda-note-images"],
description: "図にはキャプションとIDを指定してください",
tags: ["images"],
function: function LN005(params, onError) {
const { tokens } = params;
tokens.forEach(token => {
if (token.type === "inline") {
const imageMatches = token.content.match(/!\[([^\]]*)\]\([^)]+\)(?!\{#fig:)/g);
if (imageMatches) {
imageMatches.forEach(() => {
onError({
lineNumber: token.lineNumber,
detail: '図には {#fig:id} の形式でIDを指定してください。'
});
});
}
}
});
}
},
// ルール6: 表のキャプション必須
{
names: ["lambda-note-tables"],
description: "表にはキャプションを指定してください",
tags: ["tables"],
function: function LN006(params, onError) {
const { lines } = params;
let inTable = false;
let tableStartLine = 0;
lines.forEach((line, index) => {
const trimmedLine = line.trim();
if (trimmedLine.includes('|') && !inTable) {
inTable = true;
tableStartLine = index + 1;
} else if (inTable && !trimmedLine.includes('|') && trimmedLine) {
if (!trimmedLine.startsWith('Table:')) {
onError({
lineNumber: tableStartLine,
detail: 'テーブルの後には "Table: タイトル{#tbl:id}" の形式でキャプションを指定してください。'
});
}
inTable = false;
} else if (inTable && !trimmedLine) {
// 空行でテーブル終了とみなす
inTable = false;
}
});
}
},
// ルール7: 和欧文間スペース禁止
{
names: ["lambda-note-no-spaces"],
description: "日本語と英数字間にスペースを入れないでください",
tags: ["spacing"],
function: function LN007(params, onError) {
const { lines } = params;
lines.forEach((line, index) => {
// コードブロック内は除外
if (!line.includes('```') && !line.trim().startsWith('```')) {
// 日本語の後にスペース+英数字
if (/[ぁ-んァ-ヶー一-龯]\s+[a-zA-Z0-9]/.test(line)) {
onError({
lineNumber: index + 1,
detail: "日本語と英数字間にスペースを入れないでください。"
});
}
// 英数字の後にスペース+日本語
if (/[a-zA-Z0-9]\s+[ぁ-んァ-ヶー一-龯]/.test(line)) {
onError({
lineNumber: index + 1,
detail: "英数字と日本語間にスペースを入れないでください。"
});
}
}
});
}
},
// ルール8: 括弧の使用チェック
{
names: ["lambda-note-brackets"],
description: "補足には全角の丸括弧()、引用には鍵括弧「」を使用してください",
tags: ["punctuation"],
function: function LN008(params, onError) {
const { lines } = params;
lines.forEach((line, index) => {
// コードブロック内は除外
if (!line.includes('```') && !line.trim().startsWith('```')) {
// 半角括弧をチェック
if (/[ぁ-んァ-ヶー一-龯][()][ぁ-んァ-ヶー一-龯]/.test(line) ||
/[ぁ-んァ-ヶー一-龯]\([^)]*\)[ぁ-んァ-ヶー一-龯]/.test(line)) {
onError({
lineNumber: index + 1,
detail: "日本語の補足には全角の丸括弧()を使用してください。"
});
}
// ダブルクォーテーションの使用チェック
if (/"[^"]*"/.test(line) && /[ぁ-んァ-ヶー一-龯]/.test(line)) {
onError({
lineNumber: index + 1,
detail: "日本語の引用や強調には鍵括弧「」を使用してください。ダブルクォーテーションは使用しないでください。"
});
}
}
});
}
},
// ルール9: 引用ブロックの後に説明必須
{
names: ["lambda-note-quote-explanation"],
description: "引用の後には必ず説明を追加してください",
tags: ["blockquotes"],
function: function LN009(params, onError) {
const { lines } = params;
let inQuote = false;
let quoteEndLine = 0;
lines.forEach((line, index) => {
const trimmedLine = line.trim();
if (trimmedLine.startsWith('>') && !trimmedLine.startsWith('> note') &&
!trimmedLine.startsWith('> aside{')) {
inQuote = true;
} else if (inQuote && !trimmedLine.startsWith('>')) {
if (!trimmedLine) {
// 空行の場合は次の行をチェック
quoteEndLine = index + 1;
} else {
// 引用後の説明があるかチェック
if (quoteEndLine > 0 && index === quoteEndLine &&
(trimmedLine.startsWith('#') || trimmedLine.startsWith('>'))) {
onError({
lineNumber: quoteEndLine,
detail: "引用の後には必ず日本語での説明を追加してください。引用のみで説明を終わらせないでください。"
});
}
inQuote = false;
quoteEndLine = 0;
}
}
});
}
},
// ルール10: コードブロックの後に説明必須
{
names: ["lambda-note-code-explanation"],
description: "コードブロックの後には必ず説明を追加してください",
tags: ["code"],
function: function LN010(params, onError) {
const { tokens } = params;
let foundFence = false;
let fenceLineNumber = 0;
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token.type === "fence") {
foundFence = true;
fenceLineNumber = token.lineNumber;
} else if (foundFence && token.type === "paragraph_open") {
// コードブロックの後に段落があるかチェック
const nextToken = tokens[i + 1];
if (nextToken && nextToken.type === "inline" && nextToken.content.trim()) {
foundFence = false; // 説明があるのでOK
}
} else if (foundFence && (token.type === "heading_open" || token.type === "fence")) {
// 次の見出しやコードブロックが来た場合、説明がない
onError({
lineNumber: fenceLineNumber,
detail: "コードブロックの後には必ず日本語での説明を追加してください。コードの目的や動作について解説してください。"
});
foundFence = false;
}
}
}
}
];