mirror of
https://github.com/redmine/redmine.git
synced 2025-11-01 02:46:13 +01:00
Automatic list marker does not work for task list items (#43265).
Patch by Mizuki ISHIKAWA (user:ishikawa999). git-svn-id: https://svn.redmine.org/redmine/trunk@24057 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
@@ -50,67 +50,85 @@ class ListAutofillHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CommonMarkListFormatter {
|
class CommonMarkListFormatter {
|
||||||
|
// Example: ' * text' → indent=' ', bullet='*', content='text' (or '+' or '-')
|
||||||
|
#bulletItemPattern = /^(?<indent>\s*)(?<bullet>[*+\-]) (?<content>.*)$/;
|
||||||
|
// Example: ' 1. text' → indent=' ', num='1', delimiter='.', content='text' (or ')')
|
||||||
|
#orderedItemPattern = /^(?<indent>\s*)(?<num>\d+)(?<delimiter>[.)]) (?<content>.*)$/;
|
||||||
|
// Example: '[ ] Task' → taskContent='Task'
|
||||||
|
// '[x] Task' → taskContent='Task'
|
||||||
|
#taskAtStartPattern = /^\[[ x]\] (?<taskContent>.*)$/;
|
||||||
|
|
||||||
format(line) {
|
format(line) {
|
||||||
// Match list items in CommonMark syntax.
|
const bulletMatch = line.match(this.#bulletItemPattern);
|
||||||
// Captures either an ordered list (e.g., '1. ' or '2) ') or an unordered list (e.g., '* ', '- ', '+ ').
|
if (bulletMatch) {
|
||||||
// The regex structure:
|
return (
|
||||||
// ^(\s*) → leading whitespace
|
this.#formatBulletTask(bulletMatch.groups) ||
|
||||||
// (?:(\d+)([.)]) → an ordered list marker: number followed by '.' or ')'
|
this.#formatBulletList(bulletMatch.groups)
|
||||||
// |([*+\-]) → OR an unordered list marker: '*', '+', or '-'
|
);
|
||||||
// (.*)$ → the actual list item content
|
|
||||||
//
|
|
||||||
// Examples:
|
|
||||||
// '2. ordered text' → indent='', number='2', delimiter='.', bullet=undefined, content='ordered text'
|
|
||||||
// ' 3) nested ordered text' → indent=' ', number='3', delimiter=')', bullet=undefined, content='nested ordered text'
|
|
||||||
// '* unordered text' → indent='', number=undefined, delimiter=undefined, bullet='*', content='unordered text'
|
|
||||||
// '+ unordered text' → indent='', number=undefined, delimiter=undefined, bullet='+', content='unordered text'
|
|
||||||
// ' - nested unordered text' → indent=' ', number=undefined, delimiter=undefined, bullet='-', content='nested unordered text'
|
|
||||||
const match = line.match(/^(\s*)(?:(\d+)([.)])|([*+\-])) (.*)$/)
|
|
||||||
if (!match) return null
|
|
||||||
|
|
||||||
const indent = match[1]
|
|
||||||
const number = match[2]
|
|
||||||
const delimiter = match[3]
|
|
||||||
const bullet = match[4]
|
|
||||||
const content = match[5]
|
|
||||||
|
|
||||||
if (content === '') {
|
|
||||||
return { action: 'remove' }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (number) {
|
const orderedMatch = line.match(this.#orderedItemPattern);
|
||||||
const nextNumber = parseInt(number, 10) + 1
|
if (orderedMatch) {
|
||||||
return { action: 'insert', text: `${indent}${nextNumber}${delimiter} ` }
|
return (
|
||||||
} else {
|
this.#formatOrderedTask(orderedMatch.groups) ||
|
||||||
return { action: 'insert', text: `${indent}${bullet} ` }
|
this.#formatOrderedList(orderedMatch.groups)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// '- [ ] Task' or '* [ ] Task' or '+ [ ] Task'
|
||||||
|
#formatBulletTask({ indent, bullet, content }) {
|
||||||
|
const m = content.match(this.#taskAtStartPattern);
|
||||||
|
if (!m) return null;
|
||||||
|
const taskContent = m.groups.taskContent;
|
||||||
|
|
||||||
|
return taskContent === ''
|
||||||
|
? { action: 'remove' }
|
||||||
|
: { action: 'insert', text: `${indent}${bullet} [ ] ` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// '- Item' or '* Item' or '+ Item'
|
||||||
|
#formatBulletList({ indent, bullet, content }) {
|
||||||
|
return content === ''
|
||||||
|
? { action: 'remove' }
|
||||||
|
: { action: 'insert', text: `${indent}${bullet} ` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// '1. [ ] Task' or '1) [ ] Task'
|
||||||
|
#formatOrderedTask({ indent, num, delimiter, content }) {
|
||||||
|
const m = content.match(this.#taskAtStartPattern);
|
||||||
|
if (!m) return null;
|
||||||
|
const taskContent = m.groups.taskContent;
|
||||||
|
|
||||||
|
const next = `${Number(num) + 1}${delimiter}`;
|
||||||
|
return taskContent === ''
|
||||||
|
? { action: 'remove' }
|
||||||
|
: { action: 'insert', text: `${indent}${next} [ ] ` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// '1. Item' or '1) Item'
|
||||||
|
#formatOrderedList({ indent, num, delimiter, content }) {
|
||||||
|
const next = `${Number(num) + 1}${delimiter}`;
|
||||||
|
return content === ''
|
||||||
|
? { action: 'remove' }
|
||||||
|
: { action: 'insert', text: `${indent}${next} ` };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TextileListFormatter {
|
class TextileListFormatter {
|
||||||
format(line) {
|
format(line) {
|
||||||
// Match list items in Textile syntax.
|
|
||||||
// Captures either an ordered list (using '#') or an unordered list (using '*').
|
|
||||||
// The regex structure:
|
|
||||||
// ^([*#]+) → one or more list markers: '*' for unordered, '#' for ordered
|
|
||||||
// (.*)$ → the actual list item content
|
|
||||||
//
|
|
||||||
// Examples:
|
// Examples:
|
||||||
// '# ordered text' → marker='#', content='ordered text'
|
// '# ordered text' → marker='#', content='ordered text'
|
||||||
// '## nested ordered text' → marker='##', content='nested ordered text'
|
// '## nested ordered text' → marker='##', content='nested ordered text'
|
||||||
// '* unordered text' → marker='*', content='unordered text'
|
// '* unordered text' → marker='*', content='unordered text'
|
||||||
// '** nested unordered text' → marker='**', content='nested unordered text'
|
// '** nested unordered text' → marker='**', content='nested unordered text'
|
||||||
const match = line.match(/^([*#]+) (.*)$/)
|
const match = line.match(/^(?<marker>[*#]+) (?<content>.*)$/);
|
||||||
if (!match) return null
|
if (!match) return null
|
||||||
|
|
||||||
const marker = match[1]
|
const { marker, content } = match.groups;
|
||||||
const content = match[2]
|
return content === ''
|
||||||
|
? { action: 'remove' }
|
||||||
if (content === '') {
|
: { action: 'insert', text: `${marker} ` };
|
||||||
return { action: 'remove' }
|
|
||||||
}
|
|
||||||
|
|
||||||
return { action: 'insert', text: `${marker} ` }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,60 @@ class ListAutofillSystemTest < ApplicationSystemTestCase
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_autofill_with_markdown_unchecked_task_list
|
||||||
|
with_settings :text_formatting => 'common_mark' do
|
||||||
|
visit '/projects/ecookbook/issues/new'
|
||||||
|
|
||||||
|
within('form#issue-form') do
|
||||||
|
find('#issue_description').send_keys('- [ ] First item')
|
||||||
|
find('#issue_description').send_keys(:enter)
|
||||||
|
|
||||||
|
assert_equal(
|
||||||
|
"- [ ] First item\n" \
|
||||||
|
"- [ ] ",
|
||||||
|
find('#issue_description').value
|
||||||
|
)
|
||||||
|
|
||||||
|
fill_in 'Description', with: ''
|
||||||
|
find('#issue_description').send_keys('1. [ ] First item')
|
||||||
|
find('#issue_description').send_keys(:enter)
|
||||||
|
|
||||||
|
assert_equal(
|
||||||
|
"1. [ ] First item\n" \
|
||||||
|
"2. [ ] ",
|
||||||
|
find('#issue_description').value
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_autofill_with_markdown_checked_task_list
|
||||||
|
with_settings :text_formatting => 'common_mark' do
|
||||||
|
visit '/projects/ecookbook/issues/new'
|
||||||
|
|
||||||
|
within('form#issue-form') do
|
||||||
|
find('#issue_description').send_keys('- [x] First item')
|
||||||
|
find('#issue_description').send_keys(:enter)
|
||||||
|
|
||||||
|
assert_equal(
|
||||||
|
"- [x] First item\n" \
|
||||||
|
"- [ ] ",
|
||||||
|
find('#issue_description').value
|
||||||
|
)
|
||||||
|
|
||||||
|
fill_in 'Description', with: ''
|
||||||
|
find('#issue_description').send_keys('1. [x] First item')
|
||||||
|
find('#issue_description').send_keys(:enter)
|
||||||
|
|
||||||
|
assert_equal(
|
||||||
|
"1. [x] First item\n" \
|
||||||
|
"2. [ ] ",
|
||||||
|
find('#issue_description').value
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def test_textile_nested_list_autofill
|
def test_textile_nested_list_autofill
|
||||||
with_settings :text_formatting => 'textile' do
|
with_settings :text_formatting => 'textile' do
|
||||||
visit '/projects/ecookbook/issues/new'
|
visit '/projects/ecookbook/issues/new'
|
||||||
|
|||||||
Reference in New Issue
Block a user