markdown-magic 3.0.7 → 3.0.9

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.
@@ -1,9 +1,13 @@
1
1
  const fs = require('fs')
2
2
  const path = require('path')
3
- const remoteRequest = require('../utils/remoteRequest')
4
- const { isLocalPath } = require('../utils/fs')
5
- const { deepLog } = require('../utils/logs')
6
- const { getLineCount, getTextBetweenLines } = require('../utils/text')
3
+ const remoteRequest = require('../../utils/remoteRequest')
4
+ const { isLocalPath } = require('../../utils/fs')
5
+ const { deepLog } = require('../../utils/logs')
6
+ const { getLineCount, getTextBetweenLines } = require('../../utils/text')
7
+ const { resolveGithubContents, isGithubLink } = require('./resolve-github-file')
8
+
9
+ const GITHUB_LINK = /https:\/\/github\.com\/([^/\s]*)\/([^/\s]*)\/blob\//
10
+ const GIST_LINK = /https:\/\/gist\.github\.com\/([^/\s]*)\/([^/\s]*)(\/)?/
7
11
 
8
12
  /**
9
13
  * Options for specifying source code to include in documentation.
@@ -26,19 +30,26 @@ const { getLineCount, getTextBetweenLines } = require('../utils/text')
26
30
  ```
27
31
  */
28
32
 
33
+
29
34
  // TODO code sections
30
35
  // https://github.com/linear/linear/blob/94af540244864fbe466fb933256278e04e87513e/docs/transforms/code-section.js
31
36
  // https://github.com/linear/linear/blob/bc39d23af232f9fdbe7df458b0aaa9554ca83c57/packages/sdk/src/_tests/readme.test.ts#L133-L140
32
37
  // usage https://github.com/linear/linear/blame/93981d3a3db571e2f8efdce9f5271ea678941c43/packages/sdk/README.md#L1
33
38
 
34
- module.exports = function CODE(api) {
39
+ module.exports = async function CODE(api) {
35
40
  const { content, srcPath } = api
36
41
  /** @type {CodeTransformOptions} */
37
42
  const options = api.options || {}
38
43
  // console.log('CODE API', api)
39
44
  // process.exit(1)
40
- // console.log('options', options)
41
- const { src, lines, id } = options
45
+ const {
46
+ id,
47
+ lines,
48
+ isPrivate,
49
+ accessToken
50
+ } = options
51
+
52
+ let src = options.src
42
53
  const originalContent = content
43
54
  let code
44
55
  let syntax = options.syntax
@@ -61,20 +72,43 @@ module.exports = function CODE(api) {
61
72
  syntax = path.extname(codeFilePath).replace(/^./, '')
62
73
  }
63
74
  } else {
64
- // do remote request
65
- // console.log(src)
66
- const remoteContent = remoteRequest(src)
75
+ /* Automatically get raw code files from github */
76
+ // Convert https://github.com/DavidWells/markdown-magic/blob/master/package.json
77
+ // to https://raw.githubusercontent.com/DavidWells/markdown-magic/master/package.json
78
+
79
+ if (src.match(GITHUB_LINK)) {
80
+ src = src.replace(GITHUB_LINK, 'https://raw.githubusercontent.com/$1/$2/')
81
+ }
82
+ /* Automatically get raw code files from gist... needs api call.... */
83
+ // https://gist.github.com/DavidWells/7d2e0e1bc78f4ac59a123ddf8b74932d
84
+ // https://gist.githubusercontent.com/DavidWells/7d2e0e1bc78f4ac59a123ddf8b74932d/raw/0808a83de7f07c931fb81ed691c1d6bbafad29d1/aligning-images.md
85
+
86
+ let remoteContent
87
+
88
+ if (isGithubLink(src)) {
89
+ remoteContent = await resolveGithubContents({
90
+ repoFilePath: src,
91
+ accessToken,
92
+ // debug: true
93
+ })
94
+ }
95
+
96
+ // Try initial remote request if public url
97
+ if (!remoteContent) {
98
+ remoteContent = remoteRequest(src)
99
+ }
100
+
67
101
  if (!remoteContent) {
68
- console.log(`WARNING: ${src} URL NOT FOUND or internet connection is off`)
102
+ console.log(`WARNING: ${src} URL NOT FOUND or internet connection is off or no access to remove URL`)
69
103
  return originalContent
70
104
  }
71
105
  code = remoteContent
72
- syntax = path.extname(src).replace(/^./, '')
106
+ syntax = (path.extname(src).replace(/^./, '') || '').split('#')[0]
73
107
  }
74
108
 
75
109
  /* handle option `lines` */
76
110
  if (options.lines) {
77
- const lineCount = getLineCount(code)
111
+ // const lineCount = getLineCount(code)
78
112
  // console.log('lineCount', lineCount)
79
113
  // console.log('src', src)
80
114
  // console.log('lines', lines)
@@ -0,0 +1,362 @@
1
+ const https = require('https')
2
+ const { exec } = require('child_process')
3
+ const { getTextBetweenLines } = require('../../utils/text')
4
+
5
+ const VALID_SLUG_REGEX = /^[A-Z-a-z0-9_-]*$/
6
+ const VALID_FILE_REGEX = /^[^;]*$/
7
+ const GITHUB_LINK_REGEX = /^(?:https:\/\/)?github\.com\/([^/\s]*)\/([^/\s]*)\/blob\/([^/\s]*)\/([^\s]*)/
8
+ const GITHUB_RAW_LINK_REGEX = /^(?:https:\/\/)?raw\.githubusercontent\.com\/([^/\s]*)\/([^/\s]*)\/([^/\s]*)\/([^\s]*)/
9
+
10
+ function isGithubLink(str = '') {
11
+ return isGithubRepoLink(str) || isGithubRawLink(str)
12
+ }
13
+
14
+ function isGithubRepoLink(str = '') {
15
+ return GITHUB_LINK_REGEX.test(str)
16
+ }
17
+
18
+ function isGithubRawLink(str = '') {
19
+ return GITHUB_RAW_LINK_REGEX.test(str)
20
+ }
21
+
22
+ function convertLinkToRaw(link) {
23
+ if (!isGithubRepoLink(link)) return link
24
+ return link.replace(GITHUB_LINK_REGEX, 'https://raw.githubusercontent.com/$1/$2/$3/$4')
25
+ }
26
+
27
+ function resolveGithubDetails(repoFilePath) {
28
+ let parts
29
+ if (isGithubRepoLink(repoFilePath)) {
30
+ parts = repoFilePath.match(GITHUB_LINK_REGEX)
31
+ }
32
+ if (isGithubRawLink(repoFilePath)) {
33
+ parts = repoFilePath.match(GITHUB_RAW_LINK_REGEX)
34
+ }
35
+ if (!parts) {
36
+ return
37
+ }
38
+ const [ _match, repoOwner, repoName, branchOrRef, filePath ] = parts
39
+ const [ filePathStart, hash ] = filePath.split('#')
40
+ const result = {
41
+ repoOwner,
42
+ repoName,
43
+ filePath: filePathStart,
44
+ }
45
+ if (isGitHash(branchOrRef)) {
46
+ result.ref = branchOrRef
47
+ } else {
48
+ result.branch = branchOrRef
49
+ }
50
+ if (hash) {
51
+ const range = parseLineRange(`#${hash}`)
52
+ if (range) {
53
+ result.range = range
54
+ }
55
+ }
56
+ return result
57
+ }
58
+
59
+ /**
60
+ * Resolves the contents of a file from a GitHub repository.
61
+ *
62
+ * @param {Object} options - The options for resolving the GitHub contents.
63
+ * @param {string} options.repoFilePath - The file path in the GitHub repository.
64
+ * @param {string} [options.accessToken] - The access token for authenticating with GitHub (optional).
65
+ * @param {boolean} [options.debug = false] - Whether to enable debug logging (optional).
66
+ * @returns {Promise<string>} - A promise that resolves to the contents of the file.
67
+ * @throws {Error} - If the GitHub link is invalid or if the file fetch fails.
68
+ */
69
+ async function resolveGithubContents({
70
+ repoFilePath,
71
+ accessToken,
72
+ debug = false
73
+ }) {
74
+ const token = resolveAccessToken(accessToken)
75
+ const logger = (debug) ? console.log : () => {}
76
+ const githubDetails = resolveGithubDetails(repoFilePath)
77
+ if (!githubDetails) {
78
+ throw new Error(`Invalid github link. "${repoFilePath}" is not a valid github link`)
79
+ }
80
+
81
+ logger(`Github File Details "${repoFilePath}"`, githubDetails)
82
+
83
+ const payload = {
84
+ ...githubDetails,
85
+ accessToken: token
86
+ }
87
+
88
+ let errs = []
89
+
90
+ /* Try raw request first */
91
+ try {
92
+ const fileContent = await getGitHubFileContentsRaw(payload)
93
+ logger(`✅ GitHub file resolved via raw GET`)
94
+ return returnCode(fileContent, githubDetails.range)
95
+ } catch (err) {
96
+ logger('❌ Unable to resolve GitHub raw content')
97
+ errs.push(err)
98
+ }
99
+
100
+ /* Then try Github CLI or GitHub API */
101
+ const githubFetcher = (!token) ? getGitHubFileContentsCli : getGitHubFileContentsApi
102
+ try {
103
+ const fileContent = await githubFetcher(payload)
104
+ logger(`✅ GitHub file resolved via ${githubFetcher.name}`)
105
+ return returnCode(fileContent, githubDetails.range)
106
+ } catch (err) {
107
+ logger(`❌ Unable to resolve GitHub file via ${githubFetcher.name}`)
108
+ errs.push(err)
109
+ }
110
+
111
+ /* Then try API */
112
+ try {
113
+ const fileContent = await getGitHubFileContentsApi(payload)
114
+ logger(`✅ GitHub file resolved via ${getGitHubFileContentsApi.name}`)
115
+ return returnCode(fileContent, githubDetails.range)
116
+ } catch (err) {
117
+ logger(`❌ Unable to resolve GitHub file via ${githubFetcher.name}`)
118
+ errs.push(err)
119
+ }
120
+
121
+ throw new Error(`Failed to fetch GitHub file "${repoFilePath}". \n${errs.forEach(err => err.message)}`)
122
+ }
123
+
124
+ function returnCode(fileContent, lines) {
125
+ if (!lines) return fileContent
126
+ const [startLine, endLine] =lines
127
+ return getTextBetweenLines(fileContent, startLine, endLine)
128
+ }
129
+
130
+ /**
131
+ * Retrieves the contents of a file from a GitHub repository using the GitHub CLI.
132
+ *
133
+ * @param {Object} options - The options for retrieving the file contents.
134
+ * @param {string} options.repoOwner - The owner of the GitHub repository.
135
+ * @param {string} options.repoName - The name of the GitHub repository.
136
+ * @param {string} options.filePath - The path to the file in the repository.
137
+ * @param {string} [options.branch] - The branch name of the repository.
138
+ * @param {string} [options.ref] - The ref of the repository.
139
+ * @returns {Promise<string>} A promise that resolves with the decoded content of the file.
140
+ * @throws {Error} If there is an error retrieving the file contents.
141
+ */
142
+ async function getGitHubFileContentsCli(options) {
143
+ validateInputs(options)
144
+ const {
145
+ repoOwner,
146
+ repoName,
147
+ filePath,
148
+ branch,
149
+ ref,
150
+ } = options
151
+
152
+ let flags = ''
153
+ if (ref) {
154
+ flags = `?ref=${ref}`
155
+ }
156
+ if (branch) {
157
+ flags = `?ref=${branch}`
158
+ }
159
+ const command = `gh api repos/${repoOwner}/${repoName}/contents/${filePath}${flags}`
160
+ /*
161
+ console.log('command', command)
162
+ /** */
163
+ return new Promise((resolve, reject) => {
164
+ exec(command, (error, stdout, stderr) => {
165
+ if (error) {
166
+ return reject(error)
167
+ }
168
+ const fileContent = JSON.parse(stdout).content;
169
+ const decodedContent = decode(fileContent)
170
+ return resolve(decodedContent)
171
+ })
172
+ })
173
+ }
174
+
175
+ /**
176
+ * Retrieves the contents of a file from a GitHub repository via the github API
177
+ *
178
+ * @param {Object} options - The options for retrieving the file contents.
179
+ * @param {string} options.repoOwner - The owner of the GitHub repository.
180
+ * @param {string} options.repoName - The name of the GitHub repository.
181
+ * @param {string} options.filePath - The path to the file in the repository.
182
+ * @param {string} [options.branch] - The branch name to fetch the file from. If not provided, the default branch will be used.
183
+ * @param {string} [options.ref] - The ref (commit SHA or branch name) to fetch the file from. If provided, it takes precedence over the branch.
184
+ * @param {string} [options.accessToken] - The access token for authenticating the request (optional).
185
+ * @returns {Promise<string>} A promise that resolves with the decoded contents of the file.
186
+ * @throws {Error} If the file retrieval fails or the response status code is not 200.
187
+ */
188
+ function getGitHubFileContentsApi(options) {
189
+ validateInputs(options)
190
+
191
+ const {
192
+ repoOwner,
193
+ repoName,
194
+ filePath,
195
+ branch,
196
+ ref,
197
+ accessToken
198
+ } = options
199
+
200
+ let flags = ''
201
+ if (ref) {
202
+ flags = `?ref=${ref}`
203
+ }
204
+ if (branch) {
205
+ flags = `?branch=${branch}`
206
+ }
207
+ const apiEndpoint = `/repos/${repoOwner}/${repoName}/contents/${filePath}${flags}`
208
+ /*
209
+ // console.log('apiEndpoint', apiEndpoint)
210
+ /** */
211
+ return new Promise((resolve, reject) => {
212
+ const options = {
213
+ hostname: 'api.github.com',
214
+ path: apiEndpoint,
215
+ method: 'GET',
216
+ headers: {
217
+ 'User-Agent': 'Node.js',
218
+ ...(accessToken) ? { 'Authorization': `token ${accessToken}` } : {},
219
+ }
220
+ }
221
+ /*
222
+ console.log('getGitHubFileContentsApi options', options)
223
+ /** */
224
+ const req = https.request(options, (res) => {
225
+ let data = ''
226
+ res.on('data', (chunk) => {
227
+ data += chunk
228
+ })
229
+ res.on('end', () => {
230
+ if (res.statusCode === 200) {
231
+ const fileContent = JSON.parse(data).content
232
+ const decodedContent = decode(fileContent)
233
+ resolve(decodedContent)
234
+ } else {
235
+ reject(new Error(`Failed to fetch file. Status code: ${res.statusCode}`))
236
+ }
237
+ })
238
+ })
239
+
240
+ req.on('error', (error) => {
241
+ reject(error)
242
+ })
243
+
244
+ req.end()
245
+ })
246
+ }
247
+
248
+ function getGitHubFileContentsRaw(options) {
249
+ validateInputs(options)
250
+
251
+ const {
252
+ repoOwner,
253
+ repoName,
254
+ filePath,
255
+ branch,
256
+ accessToken
257
+ } = options
258
+
259
+ const [ _filePath, hash ] = filePath.split('#')
260
+ return new Promise((resolve, reject) => {
261
+ const options = {
262
+ hostname: 'raw.githubusercontent.com',
263
+ path: `/${repoOwner}/${repoName}/${branch}/${_filePath}`,
264
+ method: 'GET',
265
+ headers: {
266
+ 'User-Agent': 'Node.js',
267
+ ...(accessToken) ? { 'Authorization': `token ${accessToken}` } : {},
268
+ }
269
+ }
270
+ /*
271
+ console.log('getGitHubFileContentsRaw options', options)
272
+ /** */
273
+ const req = https.request(options, (res) => {
274
+ let data = ''
275
+ res.on('data', chunk => {
276
+ data += chunk
277
+ })
278
+
279
+ res.on('end', () => {
280
+ if (res.statusCode === 200) {
281
+ resolve(data)
282
+ } else {
283
+ reject(new Error(`Failed to fetch file. Status code: ${res.statusCode}`))
284
+ }
285
+ })
286
+ })
287
+
288
+ req.on('error', error => {
289
+ reject(error)
290
+ })
291
+
292
+ req.end()
293
+ })
294
+ }
295
+
296
+ /**
297
+ * Validates the inputs for a repository operation.
298
+ *
299
+ * @param {Object} inputs - The inputs for the repository operation.
300
+ * @param {string} inputs.repoOwner - The owner of the repository.
301
+ * @param {string} inputs.repoName - The name of the repository.
302
+ * @param {string} inputs.filePath - The file path.
303
+ * @param {string} [inputs.branch] - The branch name.
304
+ * @param {string} [inputs.ref] - The Git reference.
305
+ * @throws {Error} If any of the inputs are invalid.
306
+ */
307
+ function validateInputs({
308
+ repoOwner,
309
+ repoName,
310
+ filePath,
311
+ branch,
312
+ ref,
313
+ }) {
314
+ if (!VALID_SLUG_REGEX.test(repoOwner)) {
315
+ throw new Error(`Invalid repoOwner "${repoOwner}"`)
316
+ }
317
+ if (!VALID_SLUG_REGEX.test(repoName)) {
318
+ throw new Error(`Invalid repoName "${repoName}"`)
319
+ }
320
+ if (!VALID_FILE_REGEX.test(filePath)) {
321
+ throw new Error(`Invalid filePath "${filePath}"`)
322
+ }
323
+ if (branch && !VALID_FILE_REGEX.test(branch)) {
324
+ throw new Error(`Invalid branch "${branch}"`)
325
+ }
326
+ if (ref && !isGitHash(ref)) {
327
+ throw new Error(`Invalid ref "${ref}"`)
328
+ }
329
+ }
330
+
331
+ function resolveAccessToken(accessToken) {
332
+ if (typeof accessToken === 'string' && accessToken.match(/process\.env\./)) {
333
+ return process.env[accessToken.replace('process.env.', '')]
334
+ }
335
+ return accessToken || process.env.GITHUB_ACCESS_TOKEN
336
+ }
337
+
338
+ function decode(fileContent) {
339
+ return Buffer.from(fileContent, 'base64').toString('utf-8')
340
+ }
341
+
342
+ function isGitHash(str) {
343
+ // Regular expression to match Git hashes
344
+ const gitHashRegex = /^[vV]?[0-9a-fA-F]{40}$/
345
+ return gitHashRegex.test(str)
346
+ }
347
+
348
+ function parseLineRange(lineRangeString) {
349
+ const matches = lineRangeString.match(/#L(\d+)-L(\d+)/)
350
+ if (!matches) return
351
+ const startLine = parseInt(matches[1])
352
+ const endLine = parseInt(matches[2])
353
+ return [startLine, endLine]
354
+ }
355
+
356
+ module.exports = {
357
+ isGithubLink,
358
+ isGithubRawLink,
359
+ getGitHubFileContentsRaw,
360
+ resolveGithubDetails,
361
+ resolveGithubContents,
362
+ }
@@ -0,0 +1,32 @@
1
+ const { resolveGithubContents, getGitHubFileContentsRaw } = require('./resolve-github-file')
2
+
3
+ let repoFilePath
4
+ repoFilePath = 'https://github.com/DavidWells/markdown-magic/blob/master/package.json'
5
+ // repoFilePath = 'https://github.com/DavidWells/notes/blob/master/cognito.md'
6
+ // repoFilePath = 'github.com/DavidWells/notes/blob/master/cognito.md#L1-L5'
7
+ repoFilePath = 'github.com/DavidWells/notes/blob/master/cognito.md'
8
+ // repoFilePath = 'https://raw.githubusercontent.com/DavidWells/notes/master/cognito.md'
9
+ // repoFilePath = 'raw.githubusercontent.com/DavidWells/notes/master/cognito.md'
10
+ // repoFilePath = 'https://github.com/reapit/foundations/blob/53b2be65ea69d5f1338dbea6e5028c7599d78cf7/packages/connect-session/src/browser/index.ts#L125-L163'
11
+
12
+ /*
13
+ resolveGithubContents({
14
+ repoFilePath,
15
+ debug: true,
16
+ //accessToken: process.env.GITHUB_LAST_EDITED_TOKEN
17
+ })
18
+ .then(console.log)
19
+ .catch(console.error);
20
+ /** */
21
+
22
+ /*
23
+ getGitHubFileContentsRaw({
24
+ repoOwner: 'DavidWells',
25
+ repoName: 'notes',
26
+ filePath: 'cognito.md',
27
+ branch: 'master',
28
+ accessToken: process.env.GITHUB_LAST_EDITED_TOKEN
29
+ })
30
+ .then(console.log)
31
+ .catch(console.error);
32
+ /** */
package/lib/utils/fs.js CHANGED
@@ -2,7 +2,7 @@ const fs = require('fs').promises
2
2
  const path = require('path')
3
3
  const globrex = require('globrex')
4
4
  const isGlob = require('is-glob')
5
- const isLocalPath = require('is-local-path')
5
+ const _isLocalPath = require('is-local-path')
6
6
  const { REGEX_REGEX, escapeRegexString } = require('./regex')
7
7
  const { dirname, resolve, join } = require('path')
8
8
  const { readdir, stat, readFile } = fs
@@ -165,6 +165,11 @@ function depth(string) {
165
165
  return path.normalize(string).split(path.sep).length - 1;
166
166
  }
167
167
 
168
+ function isLocalPath(filePath) {
169
+ if (filePath.startsWith('github.com/') || filePath.startsWith('raw.githubusercontent.com/')) return false
170
+ return _isLocalPath(filePath)
171
+ }
172
+
168
173
  module.exports = {
169
174
  isLocalPath,
170
175
  writeFile,
@@ -2,13 +2,14 @@ const request = require('sync-request')
2
2
 
3
3
  module.exports = function remoteRequest(url) {
4
4
  let body
5
+ const finalUrl = (url.match(/^https?:\/\//)) ? url : `https://${url}`
5
6
  try {
6
7
  // @ts-expect-error
7
- const res = request('GET', url)
8
+ const res = request('GET', finalUrl)
8
9
  body = res.getBody('utf8')
9
10
  } catch (e) {
10
- console.log(`WARNING: REMOTE URL ${url} NOT FOUND`)
11
- console.log(e.message)
11
+ console.log(`WARNING: REMOTE URL ${finalUrl} NOT FOUND`)
12
+ console.log((e.message || '').split('\n')[0])
12
13
  }
13
14
  return body
14
15
  }
package/lib/utils/text.js CHANGED
@@ -60,6 +60,14 @@ function replaceTextBetweenChars(str = '', start, end, newStr) {
60
60
  return str.substring(0, start) + newStr + str.substring(end)
61
61
  }
62
62
 
63
+ /**
64
+ * Retrieves the text content between the specified start and end lines.
65
+ *
66
+ * @param {string} content - The content to extract text from.
67
+ * @param {number} startLine - The line number where the extraction should start.
68
+ * @param {number} endLine - The line number where the extraction should end.
69
+ * @returns {string|undefined} - The extracted text content, or undefined if both startLine and endLine are not defined.
70
+ */
63
71
  function getTextBetweenLines(content, startLine, endLine) {
64
72
  const startDefined = typeof startLine !== 'undefined'
65
73
  const endDefined = typeof endLine !== 'undefined'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "markdown-magic",
3
- "version": "3.0.7",
3
+ "version": "3.0.9",
4
4
  "description": "Automatically update markdown files with content from external sources",
5
5
  "main": "lib/index.js",
6
6
  "bin": {