@thegetty/quire-cli 1.0.0-rc.38 → 1.0.0-rc.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js CHANGED
@@ -20,7 +20,20 @@ process.removeAllListeners('warning')
20
20
  process.env.QUIRE_LOG_LEVEL = config.get('logLevel') || 'info'
21
21
 
22
22
  /**
23
- * Dynamic import ensures env var is set before logger modules are loaded
23
+ * Set NO_COLOR env var from config before importing CLI modules
24
+ *
25
+ * Chalk 5.x and ora read NO_COLOR at module load time.
26
+ * Setting it here ensures color is disabled before any styled imports.
27
+ * If NO_COLOR is already set in the shell environment, we do not override it.
28
+ *
29
+ * @see https://no-color.org/
30
+ */
31
+ if (process.env.NO_COLOR === undefined && config.get('logUseColor') === false) {
32
+ process.env.NO_COLOR = '1'
33
+ }
34
+
35
+ /**
36
+ * Dynamic import ensures env vars are set before logger modules are loaded
24
37
  */
25
38
  const { default: cli } = await import('#src/main.js')
26
39
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@thegetty/quire-cli",
3
3
  "description": "Quire command-line interface",
4
- "version": "1.0.0-rc.38",
4
+ "version": "1.0.0-rc.40",
5
5
  "author": "Getty Digital",
6
6
  "license": "SEE LICENSE IN https://github.com/thegetty/quire/blob/main/LICENSE",
7
7
  "bugs": {
@@ -70,6 +70,8 @@
70
70
  "inquirer": "^9.1.4",
71
71
  "js-yaml": "^4.1.0",
72
72
  "loglevel": "^1.8.1",
73
+ "marked": "^15.0.12",
74
+ "marked-terminal": "^7.3.0",
73
75
  "node-fetch": "^3.3.2",
74
76
  "open": "^8.4.0",
75
77
  "ora": "^6.1.2",
@@ -205,7 +205,7 @@ Examples:
205
205
  const output = formatSettings(this.config.store, {
206
206
  showInternal: options.debug,
207
207
  configPath: this.config.path,
208
- useColor: this.config.get('logUseColor'),
208
+ useColor: this.config.get('logUseColor') && !process.env.NO_COLOR,
209
209
  })
210
210
  this.logger.info(output)
211
211
  }
@@ -0,0 +1,60 @@
1
+ import Command from '#src/Command.js'
2
+ import { outputWithPaging } from '#helpers/pager.js'
3
+ import { getTopicContent, getTopicList } from '#lib/help/index.js'
4
+
5
+ /**
6
+ * Quire CLI `help` Command
7
+ *
8
+ * Display help for a specific topic or list available topics.
9
+ * Complements Commander's built-in help with extended documentation.
10
+ *
11
+ * @example
12
+ * quire help # List available topics
13
+ * quire help workflows # Show workflows topic
14
+ * quire help --list # List available topics
15
+ *
16
+ * @class HelpCommand
17
+ * @extends {Command}
18
+ */
19
+ export default class HelpCommand extends Command {
20
+ static definition = {
21
+ name: 'help',
22
+ aliases: ['h'],
23
+ description: 'Display help for a topic or list available topics',
24
+ summary: 'show help for a topic',
25
+ docsLink: 'quire-commands/',
26
+ helpText: `
27
+ Examples:
28
+ quire help List available help topics
29
+ quire help workflows Show common workflow examples
30
+ quire help pdf Show PDF generation guide
31
+ quire help --list List all available topics
32
+ `,
33
+ version: '1.0.0',
34
+ args: [
35
+ ['[topic]', 'help topic to display']
36
+ ],
37
+ options: [
38
+ ['--list', 'list all available topics'],
39
+ ],
40
+ }
41
+
42
+ constructor() {
43
+ super(HelpCommand.definition)
44
+ }
45
+
46
+ async action(topic, options, command) {
47
+ this.debug('called with topic=%s options=%O', topic, options)
48
+
49
+ // List topics if no topic specified or --list flag
50
+ if (!topic || options.list) {
51
+ const list = await getTopicList()
52
+ process.stdout.write(list + '\n')
53
+ return
54
+ }
55
+
56
+ // Display requested topic with paging (throws HelpTopicNotFoundError if missing)
57
+ const content = await getTopicContent(topic)
58
+ await outputWithPaging(content)
59
+ }
60
+ }
@@ -0,0 +1,132 @@
1
+ import esmock from 'esmock'
2
+ import sinon from 'sinon'
3
+ import test from 'ava'
4
+
5
+ /**
6
+ * Help Command Integration Tests
7
+ *
8
+ * Tests the help command behavior with mocked dependencies.
9
+ */
10
+
11
+ test.serial('help command outputs topic list when called without arguments', async (t) => {
12
+ const sandbox = sinon.createSandbox()
13
+ const stdoutWrite = sandbox.stub(process.stdout, 'write')
14
+
15
+ try {
16
+ const mockList = 'Available help topics:\n debugging Troubleshooting help'
17
+
18
+ const HelpCommand = await esmock('./help.js', {
19
+ '#lib/help/index.js': {
20
+ getTopicList: sandbox.stub().resolves(mockList),
21
+ getTopicContent: sandbox.stub()
22
+ },
23
+ '#helpers/pager.js': {
24
+ outputWithPaging: sandbox.stub()
25
+ }
26
+ })
27
+
28
+ const command = new HelpCommand.default()
29
+ await command.action(undefined, {}, {})
30
+
31
+ t.true(stdoutWrite.calledOnce)
32
+ t.true(stdoutWrite.calledWith(mockList + '\n'))
33
+ } finally {
34
+ sandbox.restore()
35
+ }
36
+ })
37
+
38
+ test.serial('help command outputs topic list when --list flag is provided', async (t) => {
39
+ const sandbox = sinon.createSandbox()
40
+ const stdoutWrite = sandbox.stub(process.stdout, 'write')
41
+
42
+ try {
43
+ const mockList = 'Available help topics:\n pdf PDF generation'
44
+
45
+ const HelpCommand = await esmock('./help.js', {
46
+ '#lib/help/index.js': {
47
+ getTopicList: sandbox.stub().resolves(mockList),
48
+ getTopicContent: sandbox.stub()
49
+ },
50
+ '#helpers/pager.js': {
51
+ outputWithPaging: sandbox.stub()
52
+ }
53
+ })
54
+
55
+ const command = new HelpCommand.default()
56
+ await command.action('ignored', { list: true }, {})
57
+
58
+ t.true(stdoutWrite.calledOnce)
59
+ t.true(stdoutWrite.calledWith(mockList + '\n'))
60
+ } finally {
61
+ sandbox.restore()
62
+ }
63
+ })
64
+
65
+ test('help command outputs topic content with paging when topic is specified', async (t) => {
66
+ const sandbox = sinon.createSandbox()
67
+
68
+ try {
69
+ const mockRendered = 'Rendered workflows content'
70
+ const mockOutputWithPaging = sandbox.stub().resolves()
71
+
72
+ const HelpCommand = await esmock('./help.js', {
73
+ '#lib/help/index.js': {
74
+ getTopicList: sandbox.stub(),
75
+ getTopicContent: sandbox.stub().resolves(mockRendered)
76
+ },
77
+ '#helpers/pager.js': {
78
+ outputWithPaging: mockOutputWithPaging
79
+ }
80
+ })
81
+
82
+ const command = new HelpCommand.default()
83
+ await command.action('workflows', {}, {})
84
+
85
+ t.true(mockOutputWithPaging.calledOnce)
86
+ t.true(mockOutputWithPaging.calledWith(mockRendered))
87
+ } finally {
88
+ sandbox.restore()
89
+ }
90
+ })
91
+
92
+ test('help command propagates HelpTopicNotFoundError from getTopicContent', async (t) => {
93
+ const sandbox = sinon.createSandbox()
94
+
95
+ try {
96
+ const { default: HelpTopicNotFoundError } = await import('#src/errors/help/help-topic-not-found-error.js')
97
+
98
+ const HelpCommand = await esmock('./help.js', {
99
+ '#lib/help/index.js': {
100
+ getTopicList: sandbox.stub(),
101
+ getTopicContent: sandbox.stub().rejects(new HelpTopicNotFoundError('nonexistent'))
102
+ },
103
+ '#helpers/pager.js': {
104
+ outputWithPaging: sandbox.stub()
105
+ }
106
+ })
107
+
108
+ const command = new HelpCommand.default()
109
+
110
+ const error = await t.throwsAsync(
111
+ () => command.action('nonexistent', {}, {}),
112
+ { name: 'HelpTopicNotFoundError' }
113
+ )
114
+
115
+ t.is(error.code, 'HELP_TOPIC_NOT_FOUND')
116
+ t.is(error.exitCode, 2)
117
+ t.is(error.topic, 'nonexistent')
118
+ } finally {
119
+ sandbox.restore()
120
+ }
121
+ })
122
+
123
+ test('help command has correct definition', async (t) => {
124
+ const HelpCommand = await import('./help.js')
125
+ const command = new HelpCommand.default()
126
+
127
+ t.is(command.name, 'help')
128
+ t.deepEqual(command.aliases, ['h'])
129
+ t.truthy(command.description)
130
+ t.truthy(command.args)
131
+ t.truthy(command.options)
132
+ })
@@ -0,0 +1,22 @@
1
+ import InvalidInputError from '../input/invalid-input-error.js'
2
+ import QuireError from '../quire-error.js'
3
+
4
+ /**
5
+ * Error thrown when a requested help topic does not exist
6
+ */
7
+ export default class HelpTopicNotFoundError extends InvalidInputError {
8
+ constructor(topic) {
9
+ super(
10
+ `Unknown help topic: ${topic}`,
11
+ {
12
+ code: 'HELP_TOPIC_NOT_FOUND',
13
+ exitCode: 2,
14
+ inputValue: topic,
15
+ inputName: 'topic',
16
+ suggestion: 'Run "quire help --list" to see available topics',
17
+ docsUrl: `${QuireError.DOCS_BASE}/quire-commands/`
18
+ }
19
+ )
20
+ this.topic = topic
21
+ }
22
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Help errors (exit code: 2)
3
+ *
4
+ * Errors related to the help system and topic lookup.
5
+ *
6
+ * @module errors/help
7
+ */
8
+ export { default as HelpTopicNotFoundError } from './help-topic-not-found-error.js'
@@ -46,3 +46,9 @@ export {
46
46
 
47
47
  // Validation errors (exit code: 4)
48
48
  export { default as ValidationError } from './validation/validation-error.js'
49
+
50
+ // Input errors (exit code: 2)
51
+ export { InvalidInputError } from './input/index.js'
52
+
53
+ // Help errors (exit code: 2)
54
+ export { HelpTopicNotFoundError } from './help/index.js'
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Input errors (exit code: 2)
3
+ *
4
+ * Errors related to invalid user input (arguments, options, values).
5
+ *
6
+ * @module errors/input
7
+ */
8
+ export { default as InvalidInputError } from './invalid-input-error.js'
@@ -0,0 +1,21 @@
1
+ import QuireError from '../quire-error.js'
2
+
3
+ /**
4
+ * Error thrown when user input is invalid
5
+ *
6
+ * Generic error for invalid arguments, options, or other user-provided values.
7
+ * Can be extended for specific input validation scenarios.
8
+ */
9
+ export default class InvalidInputError extends QuireError {
10
+ constructor(message, options = {}) {
11
+ super(message, {
12
+ code: options.code || 'INVALID_INPUT',
13
+ exitCode: options.exitCode ?? 2,
14
+ suggestion: options.suggestion,
15
+ docsUrl: options.docsUrl,
16
+ ...options
17
+ })
18
+ this.inputValue = options.inputValue
19
+ this.inputName = options.inputName
20
+ }
21
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Terminal pager utility
3
+ *
4
+ * Provides paged output for long content in interactive terminals.
5
+ * Paging can be disabled with:
6
+ * - `--no-pager` CLI flag (sets NO_PAGER env var)
7
+ * - `NO_PAGER=1` environment variable
8
+ * - `PAGER=cat` traditional Unix convention (content passes through without paging)
9
+ *
10
+ * @module helpers/pager
11
+ */
12
+ import { spawn } from 'node:child_process'
13
+
14
+ /**
15
+ * Output content with optional paging for long content
16
+ *
17
+ * Uses system pager (less/more) when:
18
+ * - Output is to an interactive terminal (TTY)
19
+ * - Content exceeds terminal height
20
+ * - Paging is not disabled via NO_PAGER env var
21
+ *
22
+ * The PAGER env var selects the pager program (default: less on Unix, more on
23
+ * Windows). Setting PAGER=cat effectively disables paging as a traditional
24
+ * Unix convention.
25
+ *
26
+ * @param {string} content - Content to output
27
+ * @returns {Promise<void>}
28
+ */
29
+ export async function outputWithPaging(content) {
30
+ const noPager = process.env.NO_PAGER
31
+ const lines = content.split('\n').length
32
+ const isTTY = process.stdout.isTTY
33
+ const terminalRows = process.stdout.rows || 24
34
+
35
+ // Use pager if content exceeds terminal height and we're in a TTY
36
+ if (!noPager && isTTY && lines > terminalRows) {
37
+ const pager = process.env.PAGER || (process.platform === 'win32' ? 'more' : 'less')
38
+ const pagerArgs = pager === 'less' ? ['-R'] : [] // -R preserves ANSI colors
39
+
40
+ return new Promise((resolve) => {
41
+ const child = spawn(pager, pagerArgs, {
42
+ stdio: ['pipe', 'inherit', 'inherit']
43
+ })
44
+
45
+ child.stdin.write(content + '\n')
46
+ child.stdin.end()
47
+
48
+ child.on('close', resolve)
49
+ child.on('error', () => {
50
+ // Fallback to direct output if pager fails
51
+ process.stdout.write(content + '\n')
52
+ resolve()
53
+ })
54
+ })
55
+ }
56
+
57
+ // Direct output for non-TTY or short content
58
+ process.stdout.write(content + '\n')
59
+ }
@@ -0,0 +1,85 @@
1
+ import sinon from 'sinon'
2
+ import test from 'ava'
3
+
4
+ /**
5
+ * Pager Helper Tests
6
+ */
7
+
8
+ test.beforeEach((t) => {
9
+ t.context.sandbox = sinon.createSandbox()
10
+ })
11
+
12
+ test.afterEach.always((t) => {
13
+ t.context.sandbox.restore()
14
+ })
15
+
16
+ test.serial('outputWithPaging() writes directly when not a TTY', async (t) => {
17
+ const { sandbox } = t.context
18
+ const originalIsTTY = process.stdout.isTTY
19
+ const stdoutWrite = sandbox.stub(process.stdout, 'write')
20
+
21
+ try {
22
+ // Simulate non-TTY (piped output)
23
+ Object.defineProperty(process.stdout, 'isTTY', { value: false, configurable: true })
24
+
25
+ const { outputWithPaging } = await import('./pager.js')
26
+ await outputWithPaging('Short content')
27
+
28
+ t.true(stdoutWrite.calledWith('Short content\n'))
29
+ } finally {
30
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, configurable: true })
31
+ }
32
+ })
33
+
34
+ test.serial('outputWithPaging() writes directly when content is short', async (t) => {
35
+ const { sandbox } = t.context
36
+ const originalIsTTY = process.stdout.isTTY
37
+ const originalRows = process.stdout.rows
38
+ const stdoutWrite = sandbox.stub(process.stdout, 'write')
39
+
40
+ try {
41
+ // Simulate TTY with 24 rows
42
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true })
43
+ Object.defineProperty(process.stdout, 'rows', { value: 24, configurable: true })
44
+
45
+ const { outputWithPaging } = await import('./pager.js')
46
+ await outputWithPaging('Line 1\nLine 2\nLine 3')
47
+
48
+ t.true(stdoutWrite.calledWith('Line 1\nLine 2\nLine 3\n'))
49
+ } finally {
50
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, configurable: true })
51
+ Object.defineProperty(process.stdout, 'rows', { value: originalRows, configurable: true })
52
+ }
53
+ })
54
+
55
+ test.serial('outputWithPaging() writes directly when NO_PAGER is set', async (t) => {
56
+ const { sandbox } = t.context
57
+ const originalIsTTY = process.stdout.isTTY
58
+ const originalRows = process.stdout.rows
59
+ const originalNoPager = process.env.NO_PAGER
60
+ const stdoutWrite = sandbox.stub(process.stdout, 'write')
61
+
62
+ try {
63
+ // Simulate TTY with small terminal (would normally trigger pager)
64
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true })
65
+ Object.defineProperty(process.stdout, 'rows', { value: 5, configurable: true })
66
+ process.env.NO_PAGER = '1'
67
+
68
+ const { outputWithPaging } = await import('./pager.js')
69
+
70
+ // Content longer than terminal height
71
+ const longContent = Array.from({ length: 20 }, (_, i) => `Line ${i + 1}`).join('\n')
72
+ await outputWithPaging(longContent)
73
+
74
+ // Should write directly despite long content, because pager is disabled
75
+ t.true(stdoutWrite.calledWith(longContent + '\n'))
76
+ } finally {
77
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, configurable: true })
78
+ Object.defineProperty(process.stdout, 'rows', { value: originalRows, configurable: true })
79
+ if (originalNoPager === undefined) {
80
+ delete process.env.NO_PAGER
81
+ } else {
82
+ process.env.NO_PAGER = originalNoPager
83
+ }
84
+ }
85
+ })
@@ -10,6 +10,8 @@ import { Argument, Option } from 'commander'
10
10
 
11
11
  // Re-export shared option definitions
12
12
  export {
13
+ colorOption,
14
+ noColorOption,
13
15
  quietOption,
14
16
  verboseOption,
15
17
  debugOption,
@@ -124,6 +124,30 @@ export const debugOption = [
124
124
  { conflicts: ['quiet'] }
125
125
  ]
126
126
 
127
+ /**
128
+ * Color options - enable/disable colored output
129
+ *
130
+ * Commander.js requires separate option definitions for --color and --no-color
131
+ * to support both the positive and negative forms. When --color has no default,
132
+ * the three-state semantics are:
133
+ *
134
+ * - `--color` → options.color = true (force color on)
135
+ * - `--no-color` → options.color = false (force color off)
136
+ * - (no flag) → options.color = undefined (falls back to env/config)
137
+ *
138
+ * Respects the NO_COLOR environment variable (https://no-color.org/).
139
+ * Respects config default: `quire settings set logUseColor false`
140
+ *
141
+ * @type {Array[]}
142
+ */
143
+ export const colorOption = [
144
+ '--color', 'force colored output (overrides NO_COLOR env var)',
145
+ ]
146
+
147
+ export const noColorOption = [
148
+ '--no-color', 'disable colored output',
149
+ ]
150
+
127
151
  /**
128
152
  * Standard output mode options (quiet, verbose, debug, progress)
129
153
  *
@@ -1,7 +1,7 @@
1
1
  import test from 'ava'
2
2
  import { Command } from 'commander'
3
3
  import { arrayToOption } from './index.js'
4
- import { quietOption, verboseOption, debugOption } from './options.js'
4
+ import { colorOption, noColorOption, quietOption, verboseOption, debugOption } from './options.js'
5
5
 
6
6
  // ─────────────────────────────────────────────────────────────────────────────
7
7
  // Integration tests for option conflicts
@@ -16,6 +16,8 @@ import { quietOption, verboseOption, debugOption } from './options.js'
16
16
  function createTestProgram() {
17
17
  const program = new Command()
18
18
  program
19
+ .addOption(arrayToOption(colorOption))
20
+ .addOption(arrayToOption(noColorOption))
19
21
  .addOption(arrayToOption(quietOption))
20
22
  .addOption(arrayToOption(verboseOption))
21
23
  .addOption(arrayToOption(debugOption))
@@ -107,3 +109,46 @@ test('options order does not affect conflict detection', (t) => {
107
109
  t.throws(() => program1.parse(['node', 'test', '--quiet', '--verbose']))
108
110
  t.throws(() => program2.parse(['node', 'test', '--verbose', '--quiet']))
109
111
  })
112
+
113
+ // ─────────────────────────────────────────────────────────────────────────────
114
+ // Color option tests
115
+ // ─────────────────────────────────────────────────────────────────────────────
116
+
117
+ test('--no-color sets color to false', (t) => {
118
+ const program = createTestProgram()
119
+ program.parse(['node', 'test', '--no-color'])
120
+ t.is(program.opts().color, false)
121
+ })
122
+
123
+ test('--color sets color to true', (t) => {
124
+ const program = createTestProgram()
125
+ program.parse(['node', 'test', '--color'])
126
+ t.is(program.opts().color, true)
127
+ })
128
+
129
+ test('no color flag leaves color undefined', (t) => {
130
+ const program = createTestProgram()
131
+ program.parse(['node', 'test'])
132
+ t.is(program.opts().color, undefined)
133
+ })
134
+
135
+ test('--no-color can be combined with --verbose', (t) => {
136
+ const program = createTestProgram()
137
+ t.notThrows(() => {
138
+ program.parse(['node', 'test', '--no-color', '--verbose'])
139
+ })
140
+ })
141
+
142
+ test('--no-color can be combined with --quiet', (t) => {
143
+ const program = createTestProgram()
144
+ t.notThrows(() => {
145
+ program.parse(['node', 'test', '--no-color', '--quiet'])
146
+ })
147
+ })
148
+
149
+ test('--no-color can be combined with --debug', (t) => {
150
+ const program = createTestProgram()
151
+ t.notThrows(() => {
152
+ program.parse(['node', 'test', '--no-color', '--debug'])
153
+ })
154
+ })
@@ -0,0 +1,77 @@
1
+ /**
2
+ * YAML frontmatter parsing utilities
3
+ *
4
+ * Extracts and strips YAML frontmatter from markdown content.
5
+ * Frontmatter is delimited by `---` on its own line.
6
+ *
7
+ * @module lib/help/frontmatter
8
+ */
9
+ import yaml from 'js-yaml'
10
+
11
+ /** Regex to match YAML frontmatter block */
12
+ const FRONTMATTER_REGEX = /^---\n([\s\S]*?)\n---/
13
+
14
+ /**
15
+ * Extract YAML frontmatter from markdown content
16
+ *
17
+ * @param {string} content - Markdown content with optional frontmatter
18
+ * @returns {Object} Parsed frontmatter data or empty object if none/invalid
19
+ *
20
+ * @example
21
+ * const meta = extractFrontmatter(`---
22
+ * title: My Topic
23
+ * description: A description
24
+ * ---
25
+ * # Content`)
26
+ * // => { title: 'My Topic', description: 'A description' }
27
+ */
28
+ export function extractFrontmatter(content) {
29
+ const match = content.match(FRONTMATTER_REGEX)
30
+ if (!match) return {}
31
+
32
+ try {
33
+ return yaml.load(match[1]) || {}
34
+ } catch {
35
+ return {}
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Strip YAML frontmatter from markdown content
41
+ *
42
+ * @param {string} content - Markdown content with optional frontmatter
43
+ * @returns {string} Content without frontmatter block
44
+ *
45
+ * @example
46
+ * const body = stripFrontmatter(`---
47
+ * title: My Topic
48
+ * ---
49
+ * # Content`)
50
+ * // => '# Content'
51
+ */
52
+ export function stripFrontmatter(content) {
53
+ return content.replace(/^---\n[\s\S]*?\n---\n?/, '')
54
+ }
55
+
56
+ /**
57
+ * Parse markdown content with frontmatter
58
+ *
59
+ * Convenience function that returns both metadata and content body.
60
+ *
61
+ * @param {string} content - Markdown content with optional frontmatter
62
+ * @returns {{ data: Object, content: string }} Parsed frontmatter and body
63
+ *
64
+ * @example
65
+ * const { data, content } = parseFrontmatter(`---
66
+ * title: My Topic
67
+ * ---
68
+ * # Content`)
69
+ * // data => { title: 'My Topic' }
70
+ * // content => '# Content'
71
+ */
72
+ export function parseFrontmatter(content) {
73
+ return {
74
+ data: extractFrontmatter(content),
75
+ content: stripFrontmatter(content)
76
+ }
77
+ }