Skip to content

Commit c2071b1

Browse files
authored
Merge pull request #370 from marp-team/author-and-keywords-metadata
Add `author` and `keywords` directives and CLI options for setting metadata
2 parents 0b641c2 + 9acc645 commit c2071b1

File tree

10 files changed

+122
-10
lines changed

10 files changed

+122
-10
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
- PDF metadata support ([#367](https://github.com/marp-team/marp-cli/issues/367), [#369](https://github.com/marp-team/marp-cli/pull/369))
88
- `--pdf-notes` option to add presenter notes into PDF as annotations ([#261](https://github.com/marp-team/marp-cli/issues/261), [#369](https://github.com/marp-team/marp-cli/pull/369))
9+
- `author` and `keywords` metadata options / global directives ([#367](https://github.com/marp-team/marp-cli/issues/367), [#370](https://github.com/marp-team/marp-cli/pull/370))
910

1011
## v1.2.0 - 2021-07-22
1112

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,8 @@ Through [global directives] or CLI options, you can set metadata for a converted
284284
| :-----------------: | :-------------: | :------------------------------ | :-------------- |
285285
| `title` | `--title` | Define title of the slide deck | HTML, PDF, PPTX |
286286
| `description` | `--description` | Define description of the slide | HTML, PDF, PPTX |
287+
| `author` | `--author` | Define author of the slide deck | HTML, PDF, PPTX |
288+
| `keywords` | `--keywords` | Define comma-separated keywords | HTML, PDF |
287289
| `url` | `--url` | Define [canonical URL] \* | HTML |
288290
| `image` | `--og-image` | Define [Open Graph] image URL | HTML |
289291

@@ -300,6 +302,8 @@ Marp CLI supports _additional [global directives]_ to specify metadata in Markdo
300302
---
301303
title: Marp slide deck
302304
description: An example slide deck created by Marp CLI
305+
author: Yuki Hattori
306+
keywords: marp,marp-cli,slide
303307
url: https://marp.app/
304308
image: https://marp.app/og-image.jpg
305309
---
@@ -311,7 +315,7 @@ image: https://marp.app/og-image.jpg
311315

312316
### By CLI option
313317

314-
Marp CLI prefers CLI option to global directives. You can override metadata values by `--title`, `--description`, `--url`, and `--og-image`.
318+
Marp CLI prefers CLI option to global directives. You can override metadata values by `--title`, `--description`, `--author`, `--keywords`, `--url`, and `--og-image`.
315319

316320
## Theme
317321

@@ -445,6 +449,7 @@ If you want to prevent looking up a configuration file, you can pass `--no-confi
445449
| Key | Type | CLI option | Description |
446450
| :---------------- | :-------------------------: | :-------------------: | :----------------------------------------------------------------------------------------------------- |
447451
| `allowLocalFiles` | boolean | `--allow-local-files` | Allow to access local files from Markdown while converting PDF _(NOT SECURE)_ |
452+
| `author` | string | `--author` | Define author of the slide deck |
448453
| `bespoke` | object | | Setting options for `bespoke` template |
449454
|`osc` | boolean | `--bespoke.osc` | \[Bespoke\] Use on-screen controller (`true` by default) |
450455
|`progress` | boolean | `--bespoke.progress` | \[Bespoke\] Use progress bar (`false` by default) |
@@ -456,6 +461,7 @@ If you want to prevent looking up a configuration file, you can pass `--no-confi
456461
| `imageScale` | number | `--image-scale` | The scale factor for rendered images (`1` by default, or `2.5` for PPTX conversion) |
457462
| `inputDir` | string | `--input-dir` `-I` | The base directory to find markdown and theme CSS |
458463
| `jpegQuality` | number | `--jpeg-quality` | Setting JPEG image quality (`85` by default) |
464+
| `keywords` | string \| string[] | `--keywords` | Define keywords for the slide deck (Accepts comma-separated string and array of string) |
459465
| `lang` | string | | Define the language of converted HTML |
460466
| `ogImage` | string | `--og-image` | Define [Open Graph] image URL |
461467
| `options` | object | | The base options for the constructor of engine |

src/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import osLocale from 'os-locale'
88
import { info, warn } from './cli'
99
import { ConverterOption, ConvertType } from './converter'
1010
import resolveEngine, { ResolvableEngine, ResolvedEngine } from './engine'
11+
import { keywordsAsArray } from './engine/meta-plugin'
1112
import { error } from './error'
1213
import { TemplateOption } from './templates'
1314
import { Theme, ThemeSet } from './theme'
@@ -17,6 +18,7 @@ type Overwrite<T, U> = Omit<T, Extract<keyof T, keyof U>> & U
1718
interface IMarpCLIArguments {
1819
_?: string[]
1920
allowLocalFiles?: boolean
21+
author?: string
2022
baseUrl?: string
2123
bespoke?: {
2224
osc?: boolean
@@ -31,6 +33,7 @@ interface IMarpCLIArguments {
3133
imageScale?: number
3234
inputDir?: string
3335
jpegQuality?: number
36+
keywords?: string
3437
ogImage?: string
3538
output?: string | false
3639
pdf?: boolean
@@ -51,6 +54,7 @@ export type IMarpCLIConfig = Overwrite<
5154
{
5255
engine?: ResolvableEngine | ResolvableEngine[]
5356
html?: ConverterOption['html']
57+
keywords?: string | string[]
5458
lang?: string
5559
options?: ConverterOption['options']
5660
themeSet?: string | string[]
@@ -211,8 +215,10 @@ export class MarpCLIConfig {
211215
baseUrl: this.args.baseUrl ?? this.conf.baseUrl,
212216
engine: this.engine.klass,
213217
globalDirectives: {
218+
author: this.args.author ?? this.conf.author,
214219
description: this.args.description ?? this.conf.description,
215220
image: this.args.ogImage ?? this.conf.ogImage,
221+
keywords: keywordsAsArray(this.args.keywords ?? this.conf.keywords),
216222
theme: theme instanceof Theme ? theme.name : theme,
217223
title: this.args.title ?? this.conf.title,
218224
url: this.args.url ?? this.conf.url,

src/converter.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,9 @@ export class Converter {
269269

270270
if (tpl.rendered.title) pdfDoc.setTitle(tpl.rendered.title)
271271
if (tpl.rendered.description) pdfDoc.setSubject(tpl.rendered.description)
272+
if (tpl.rendered.author) pdfDoc.setAuthor(tpl.rendered.author)
273+
if (tpl.rendered.keywords)
274+
pdfDoc.setKeywords([tpl.rendered.keywords.join('; ')])
272275

273276
if (this.options.pdfNotes) {
274277
const pages = pdfDoc.getPages()
@@ -282,7 +285,9 @@ export class Converter {
282285
Subtype: 'Text',
283286
Rect: [0, 20, 20, 20],
284287
Contents: PDFHexString.fromText(notes),
285-
// Title: PDFString.of('Author'), // TODO: Set author
288+
T: tpl.rendered.author
289+
? PDFHexString.fromText(tpl.rendered.author)
290+
: undefined,
286291
Name: 'Note',
287292
Subj: PDFString.of('Note'),
288293
C: [1, 0.92, 0.42], // RGB
@@ -377,7 +382,7 @@ export class Converter {
377382
const pptx = new (await import('pptxgenjs')).default()
378383
const layoutName = `${tpl.rendered.size.width}x${tpl.rendered.size.height}`
379384

380-
pptx.author = CREATED_BY_MARP
385+
pptx.author = tpl.rendered.author ?? CREATED_BY_MARP
381386
pptx.company = CREATED_BY_MARP
382387

383388
pptx.defineLayout({

src/engine/info-plugin.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
export interface EngineInfo {
2-
theme: string | undefined
2+
author: string | undefined
33
description: string | undefined
44
image: string | undefined
5+
keywords: string[] | undefined
56
length: number
67
size: { height: number; width: number }
8+
theme: string | undefined
79
title: string | undefined
810
url: string | undefined
911
}
@@ -22,8 +24,10 @@ export default function infoPlugin(md: any) {
2224

2325
const info: EngineInfo = {
2426
theme,
27+
author: globalDirectives.marpCLIAuthor,
2528
description: globalDirectives.marpCLIDescription,
2629
image: globalDirectives.marpCLIImage,
30+
keywords: globalDirectives.marpCLIKeywords,
2731
title: globalDirectives.marpCLITitle,
2832
url: globalDirectives.marpCLIURL,
2933
size: {

src/engine/meta-plugin.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,40 @@ import { URL } from 'url'
22
import { Marpit } from '@marp-team/marpit'
33
import { warn } from '../cli'
44

5+
export const keywordsAsArray = (keywords: unknown): string[] | undefined => {
6+
let kws: any[] | undefined
7+
8+
if (Array.isArray(keywords)) {
9+
kws = keywords
10+
} else if (typeof keywords === 'string') {
11+
kws = keywords.split(',').map((s) => s.trim())
12+
}
13+
14+
if (kws) {
15+
const filtered = [
16+
...new Set(
17+
kws.filter(
18+
(kw: unknown): kw is string => typeof kw === 'string' && !!kw
19+
)
20+
).values(),
21+
]
22+
23+
if (filtered.length > 0) return filtered
24+
}
25+
26+
return undefined
27+
}
28+
529
export default function metaPlugin({ marpit }: { marpit: Marpit }) {
630
Object.assign(marpit.customDirectives.global, {
31+
author: (v) => (typeof v === 'string' ? { marpCLIAuthor: v } : {}),
732
description: (v) =>
833
typeof v === 'string' ? { marpCLIDescription: v } : {},
934
image: (v) => (typeof v === 'string' ? { marpCLIImage: v } : {}),
35+
keywords: (v) => {
36+
const marpCLIKeywords = keywordsAsArray(v)
37+
return marpCLIKeywords ? { marpCLIKeywords } : {}
38+
},
1039
title: (v) => (typeof v === 'string' ? { marpCLITitle: v } : {}),
1140
url: (v) => {
1241
// URL validation

src/marp-cli.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ enum OptionGroup {
1717
Basic = 'Basic Options:',
1818
Converter = 'Converter Options:',
1919
Template = 'Template Options:',
20-
Meta = 'Meta Options:',
20+
Meta = 'Metadata Options:',
2121
Marp = 'Marp / Marpit Options:',
2222
}
2323

@@ -190,6 +190,16 @@ export const marpCli = async (
190190
group: OptionGroup.Meta,
191191
type: 'string',
192192
},
193+
author: {
194+
describe: 'Define author of the slide deck',
195+
group: OptionGroup.Meta,
196+
type: 'string',
197+
},
198+
keywords: {
199+
describe: 'Define comma-separated keywords for the slide deck',
200+
group: OptionGroup.Meta,
201+
type: 'string',
202+
},
193203
url: {
194204
describe: 'Define canonical URL',
195205
group: OptionGroup.Meta,

src/templates/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ interface TemplateCoreOption {
2020
}
2121

2222
export interface TemplateMeta {
23+
author: string | undefined
2324
description: string | undefined
2425
image: string | undefined
26+
keywords: string[] | undefined
2527
title: string | undefined
2628
url: string | undefined
2729
}

src/templates/layout.pug

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,17 @@ html(lang=lang)
1111
if image
1212
meta(property="og:image:alt", content=title)
1313

14+
if author
15+
meta(name="author", content=author)
16+
meta(property="article:author", content=author)
17+
1418
if description
1519
meta(name="description", content=description)
1620
meta(property="og:description", content=description)
1721

22+
if keywords && keywords.length > 1
23+
meta(name="keywords", content=keywords.join(','))
24+
1825
if url
1926
link(rel="canonical", href=url)
2027
meta(property="og:url", content=url)

test/converter.ts

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import Marp from '@marp-team/marp-core'
77
import { Options } from '@marp-team/marpit'
88
import cheerio from 'cheerio'
99
import { imageSize } from 'image-size'
10-
import { PDFDocument, PDFDict, PDFName, PDFString, PDFHexString } from 'pdf-lib'
10+
import { PDFDocument, PDFDict, PDFName, PDFHexString } from 'pdf-lib'
1111
import { Page } from 'puppeteer-core/lib/cjs/puppeteer/common/Page'
1212
import yauzl from 'yauzl'
1313
import { Converter, ConvertType, ConverterOption } from '../src/converter'
@@ -139,13 +139,19 @@ describe('Converter', () => {
139139
globalDirectives: {
140140
title: 'Title',
141141
description: 'Desc',
142+
author: 'Author',
143+
keywords: ['a', '"b"', 'c'],
142144
url: 'https://example.com/canonical',
143145
image: 'https://example.com/image.jpg',
144146
},
145147
}).convert('---\ntitle: original\n---')
146148

147149
expect(result).toContain('<title>Title</title>')
148150
expect(result).toContain('<meta name="description" content="Desc">')
151+
expect(result).toContain('<meta name="author" content="Author">')
152+
expect(result).toContain(
153+
'<meta name="keywords" content="a,&quot;b&quot;,c">'
154+
)
149155
expect(result).toContain(
150156
'<link rel="canonical" href="https://example.com/canonical">'
151157
)
@@ -154,15 +160,24 @@ describe('Converter', () => {
154160
)
155161
})
156162

157-
it('allows reset meta values by empty string', async () => {
163+
it('allows reset meta values by empty string / array', async () => {
158164
const { result } = await instance({
159-
globalDirectives: { title: '', description: '', url: '', image: '' },
165+
globalDirectives: {
166+
title: '',
167+
description: '',
168+
author: '',
169+
keywords: [],
170+
url: '',
171+
image: '',
172+
},
160173
}).convert(
161-
'---\ntitle: A\ndescription: B\nurl: https://example.com/\nimage: /hello.jpg\n---'
174+
'---\ntitle: A\ndescription: B\nauthor: C\nkeywords: D\nurl: https://example.com/\nimage: /hello.jpg\n---'
162175
)
163176

164177
expect(result).not.toContain('<title>')
165178
expect(result).not.toContain('<meta name="description"')
179+
expect(result).not.toContain('<meta name="author"')
180+
expect(result).not.toContain('<meta name="keywords"')
166181
expect(result).not.toContain('<link rel="canonical"')
167182
expect(result).not.toContain('<meta property="og:image"')
168183
})
@@ -304,13 +319,20 @@ describe('Converter', () => {
304319

305320
await pdfInstance({
306321
output: 'test.pdf',
307-
globalDirectives: { title: 'title', description: 'description' },
322+
globalDirectives: {
323+
title: 'title',
324+
description: 'description',
325+
author: 'author',
326+
keywords: ['a', 'b', 'c'],
327+
},
308328
}).convertFile(new File(onePath))
309329

310330
const pdf = await PDFDocument.load(write.mock.calls[0][1])
311331

312332
expect(pdf.getTitle()).toBe('title')
313333
expect(pdf.getSubject()).toBe('description')
334+
expect(pdf.getAuthor()).toBe('author')
335+
expect(pdf.getKeywords()).toBe('a; b; c')
314336
},
315337
puppeteerTimeoutMs
316338
)
@@ -377,6 +399,24 @@ describe('Converter', () => {
377399
},
378400
puppeteerTimeoutMs
379401
)
402+
403+
it('sets a comment author to notes if set author global directive', async () => {
404+
const write = (<any>fs).__mockWriteFile()
405+
406+
await pdfInstance({
407+
output: 'test.pdf',
408+
pdfNotes: true,
409+
globalDirectives: { author: 'author' },
410+
}).convertFile(new File(threePath))
411+
412+
const pdf = await PDFDocument.load(write.mock.calls[0][1])
413+
const annotaionRef = pdf.getPage(0).node.Annots()?.get(0)
414+
const annotation = pdf.context.lookup(annotaionRef, PDFDict)
415+
416+
expect(annotation.get(PDFName.of('T'))).toStrictEqual(
417+
PDFHexString.fromText('author')
418+
)
419+
})
380420
})
381421
})
382422

@@ -467,6 +507,7 @@ describe('Converter', () => {
467507
globalDirectives: {
468508
title: 'Test meta',
469509
description: 'Test description',
510+
author: 'author',
470511
},
471512
}).convertFile(new File(onePath))
472513

@@ -475,6 +516,7 @@ describe('Converter', () => {
475516

476517
expect(meta['dc:title']).toBe('Test meta')
477518
expect(meta['dc:subject']).toBe('Test description')
519+
expect(meta['dc:creator']).toBe('author')
478520

479521
// Custom scale
480522
expect(setViewport).toHaveBeenCalledWith(

0 commit comments

Comments
 (0)