mirror of
				https://github.com/redmine/redmine.git
				synced 2025-10-31 02:15:52 +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