-
-
Notifications
You must be signed in to change notification settings - Fork 145
Automatically Extract Title from First Heading in Markdown #655
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
yhatt
merged 7 commits into
marp-team:main
from
Alberthor47:feature/autoset-title-when-possible-on-html
Jun 11, 2025
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
dd24ed6
feat(title): helper function added to set title if no provided
Alberthor47 1869867
Merge branch 'main' of github.com:marp-team/marp-cli
Alberthor47 7ff3c50
test: test added
Alberthor47 5be140f
fix: prettier rules
Alberthor47 d6f0682
Update src/engine/info-plugin.ts
Alberthor47 6d7d61c
Merge branch 'main' of github.com:marp-team/marp-cli
Alberthor47 91d8775
Merge branch 'main' into feature/autoset-title-when-possible-on-html
Alberthor47 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,314 @@ | ||
import infoPlugin, { engineInfo } from '../../src/engine/info-plugin' | ||
|
||
describe('Engine info plugin', () => { | ||
// Helper to create mock heading tokens | ||
const createHeadingTokens = (tag: string, content: string) => [ | ||
Comment on lines
+4
to
+5
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be better to test with the result of parse by actual Marp or markdown-it instance. |
||
{ | ||
type: 'heading_open', | ||
tag, | ||
attrs: null, | ||
map: [0, 1], | ||
nesting: 1, | ||
level: 0, | ||
children: null, | ||
content: '', | ||
markup: '#', | ||
info: '', | ||
meta: null, | ||
block: true, | ||
hidden: false, | ||
}, | ||
{ | ||
type: 'inline', | ||
tag: '', | ||
attrs: null, | ||
map: [0, 1], | ||
nesting: 0, | ||
level: 1, | ||
children: [], | ||
content, | ||
markup: '', | ||
info: '', | ||
meta: null, | ||
block: true, | ||
hidden: false, | ||
}, | ||
{ | ||
type: 'heading_close', | ||
tag, | ||
attrs: null, | ||
map: null, | ||
nesting: -1, | ||
level: 0, | ||
children: null, | ||
content: '', | ||
markup: '#', | ||
info: '', | ||
meta: null, | ||
block: true, | ||
hidden: false, | ||
}, | ||
] | ||
|
||
describe('#infoPlugin title extraction', () => { | ||
const marpitMock = () => ({ | ||
customDirectives: { global: {} }, | ||
themeSet: { | ||
default: { name: 'default' }, | ||
getThemeProp: jest.fn(() => 1080), | ||
}, | ||
options: { lang: 'en' }, | ||
}) | ||
|
||
const mdMock = () => ({ | ||
core: { ruler: { push: jest.fn() } }, | ||
marpit: marpitMock(), | ||
}) | ||
|
||
// Test cases for title extraction behavior | ||
it('returns undefined when there are no tokens', () => { | ||
const md = mdMock() | ||
infoPlugin(md) | ||
|
||
const infoRule = md.core.ruler.push.mock.calls[0][1] | ||
const state = { inlineMode: false, tokens: [] } | ||
|
||
infoRule(state) | ||
|
||
expect(md.marpit[engineInfo].title).toBeUndefined() | ||
}) | ||
|
||
it('returns undefined when there are no heading tokens', () => { | ||
const md = mdMock() | ||
infoPlugin(md) | ||
|
||
const infoRule = md.core.ruler.push.mock.calls[0][1] | ||
const state = { | ||
inlineMode: false, | ||
tokens: [ | ||
{ type: 'paragraph_open' }, | ||
{ type: 'inline', content: 'Hello world' }, | ||
{ type: 'paragraph_close' }, | ||
], | ||
} | ||
|
||
infoRule(state) | ||
|
||
expect(md.marpit[engineInfo].title).toBeUndefined() | ||
}) | ||
|
||
it('extracts title from a single heading token', () => { | ||
const md = mdMock() | ||
infoPlugin(md) | ||
|
||
const infoRule = md.core.ruler.push.mock.calls[0][1] | ||
const state = { | ||
inlineMode: false, | ||
tokens: createHeadingTokens('h1', 'Slide Title'), | ||
} | ||
|
||
infoRule(state) | ||
|
||
expect(md.marpit[engineInfo].title).toBe('Slide Title') | ||
}) | ||
|
||
it('extracts title from the highest hierarchy heading', () => { | ||
const md = mdMock() | ||
infoPlugin(md) | ||
|
||
const infoRule = md.core.ruler.push.mock.calls[0][1] | ||
const state = { | ||
inlineMode: false, | ||
tokens: [ | ||
...createHeadingTokens('h2', 'Secondary Title'), | ||
...createHeadingTokens('h1', 'Main Title'), | ||
...createHeadingTokens('h3', 'Tertiary Title'), | ||
], | ||
} | ||
|
||
infoRule(state) | ||
|
||
expect(md.marpit[engineInfo].title).toBe('Main Title') | ||
}) | ||
|
||
it('extracts title from the first highest heading when multiple exist', () => { | ||
const md = mdMock() | ||
infoPlugin(md) | ||
|
||
const infoRule = md.core.ruler.push.mock.calls[0][1] | ||
const state = { | ||
inlineMode: false, | ||
tokens: [ | ||
...createHeadingTokens('h2', 'First Section'), | ||
...createHeadingTokens('h1', 'First Main Title'), | ||
...createHeadingTokens('h1', 'Second Main Title'), | ||
], | ||
} | ||
|
||
infoRule(state) | ||
|
||
expect(md.marpit[engineInfo].title).toBe('First Main Title') | ||
}) | ||
|
||
it('ignores malformed heading tokens missing content', () => { | ||
const md = mdMock() | ||
infoPlugin(md) | ||
|
||
const infoRule = md.core.ruler.push.mock.calls[0][1] | ||
const state = { | ||
inlineMode: false, | ||
tokens: [ | ||
{ | ||
type: 'heading_open', | ||
tag: 'h1', | ||
}, | ||
// Missing inline content token | ||
{ | ||
type: 'heading_close', | ||
tag: 'h1', | ||
}, | ||
...createHeadingTokens('h2', 'Secondary Title'), | ||
], | ||
} | ||
|
||
infoRule(state) | ||
|
||
expect(md.marpit[engineInfo].title).toBe('Secondary Title') | ||
}) | ||
|
||
it('trims whitespace from heading content', () => { | ||
const md = mdMock() | ||
infoPlugin(md) | ||
|
||
const infoRule = md.core.ruler.push.mock.calls[0][1] | ||
const state = { | ||
inlineMode: false, | ||
tokens: createHeadingTokens('h1', ' Title with spaces '), | ||
} | ||
|
||
infoRule(state) | ||
|
||
expect(md.marpit[engineInfo].title).toBe('Title with spaces') | ||
}) | ||
}) | ||
|
||
describe('#infoPlugin', () => { | ||
const marpitMock = () => { | ||
const marpit = { | ||
customDirectives: { global: {} }, | ||
themeSet: { | ||
default: { name: 'default' }, | ||
getThemeProp: jest.fn(() => 1080), | ||
}, | ||
options: { lang: 'en' }, | ||
} | ||
return marpit | ||
} | ||
|
||
const mdMock = () => { | ||
const md: any = { | ||
core: { ruler: { push: jest.fn() } }, | ||
marpit: marpitMock(), | ||
} | ||
return md | ||
} | ||
|
||
it('adds marp_cli_info ruler to markdown-it', () => { | ||
const md = mdMock() | ||
infoPlugin(md) | ||
|
||
expect(md.core.ruler.push).toHaveBeenCalledWith( | ||
'marp_cli_info', | ||
expect.any(Function) | ||
) | ||
}) | ||
|
||
it('returns early when in inline mode', () => { | ||
const md = mdMock() | ||
infoPlugin(md) | ||
|
||
const infoRule = md.core.ruler.push.mock.calls[0][1] | ||
const state = { inlineMode: true } | ||
|
||
expect(infoRule(state)).toBeUndefined() | ||
expect(md.marpit[engineInfo]).toBeUndefined() | ||
}) | ||
|
||
it('uses global directive title when available', () => { | ||
const md = mdMock() | ||
md.marpit.lastGlobalDirectives = { marpCLITitle: 'Title from Directive' } | ||
|
||
infoPlugin(md) | ||
|
||
const infoRule = md.core.ruler.push.mock.calls[0][1] | ||
const state = { | ||
inlineMode: false, | ||
tokens: createHeadingTokens('h1', 'Heading Title'), | ||
} | ||
|
||
infoRule(state) | ||
|
||
expect(md.marpit[engineInfo].title).toBe('Title from Directive') | ||
}) | ||
|
||
it('counts slides correctly', () => { | ||
const md = mdMock() | ||
infoPlugin(md) | ||
|
||
const infoRule = md.core.ruler.push.mock.calls[0][1] | ||
const state = { | ||
inlineMode: false, | ||
tokens: [ | ||
{ type: 'slide_open', meta: { marpitSlideElement: 1 } }, | ||
...createHeadingTokens('h1', 'Slide 1'), | ||
{ type: 'slide_close' }, | ||
{ type: 'slide_open', meta: { marpitSlideElement: 1 } }, | ||
...createHeadingTokens('h2', 'Slide 2'), | ||
{ type: 'slide_close' }, | ||
], | ||
} | ||
|
||
infoRule(state) | ||
|
||
expect(md.marpit[engineInfo]).toHaveLength(2) | ||
}) | ||
|
||
it('sets all info properties correctly', () => { | ||
const md = mdMock() | ||
md.marpit.lastGlobalDirectives = { | ||
theme: 'gaia', | ||
marpCLITitle: 'Presentation Title', | ||
marpCLIAuthor: 'Author Name', | ||
marpCLIDescription: 'Description text', | ||
marpCLIImage: 'image.png', | ||
marpCLIKeywords: ['key1', 'key2'], | ||
marpCLIURL: 'https://example.com', | ||
lang: 'es', | ||
} | ||
|
||
infoPlugin(md) | ||
|
||
const infoRule = md.core.ruler.push.mock.calls[0][1] | ||
const state = { inlineMode: false, tokens: [] } | ||
|
||
infoRule(state) | ||
|
||
const info = md.marpit[engineInfo] | ||
expect(info).toMatchObject({ | ||
theme: 'gaia', | ||
title: 'Presentation Title', | ||
author: 'Author Name', | ||
description: 'Description text', | ||
image: 'image.png', | ||
keywords: ['key1', 'key2'], | ||
url: 'https://example.com', | ||
lang: 'es', | ||
length: 0, | ||
size: { | ||
height: 1080, | ||
width: 1080, | ||
}, | ||
}) | ||
}) | ||
}) | ||
}) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the initial release is fine with this. In future releases, it would be better to use
MarkdownIt.renderInline()
to reflect the exact contents.