Skip to content

Commit 4484068

Browse files
authored
Move render-content code to this repo (#16544)
* Move render-content files in here * Replace existing file with nested index.js * Copy in tests and jest-ify * Update docs * Uninstall @github-docs/render-content * Bring over README * Add missing dependencies * Fix require paths
1 parent 9d9a694 commit 4484068

File tree

9 files changed

+724
-161
lines changed

9 files changed

+724
-161
lines changed

contributing/content-markup-reference.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
[Markdown](http://daringfireball.net/projects/markdown/) is a human-friendly syntax for formatting plain text. Our documentation is written with [GitHub Flavored Markdown](https://docs.github.com/en/github/writing-on-github/about-writing-and-formatting-on-github), a custom version of Markdown used across GitHub.
1818

19-
This site's Markdown rendering is powered by the [`@github-docs/render-content`](https://github.com/docs/render-content) and [`hubdown`](https://github.com/electron/hubdown) npm packages, which are in turn built on the [`remark`](https://remark.js.org/) Markdown processor.
19+
This site's Markdown rendering is powered by the [`/lib/render-content`](/lib/render-content) and [`hubdown`](https://github.com/electron/hubdown) npm packages, which are in turn built on the [`remark`](https://remark.js.org/) Markdown processor.
2020

2121
## Callout tags
2222

lib/render-content/README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
Markdown and Liquid rendering pipeline.
2+
3+
## Usage
4+
5+
```js
6+
const renderContent = require('.')
7+
8+
const html = await renderContent(`
9+
# Beep
10+
{{ foo }}
11+
`, {
12+
foo: 'bar'
13+
})
14+
```
15+
16+
Creates:
17+
18+
```html
19+
<h1 id="beep"><a href="#beep">Beep</a></h1>
20+
<p>bar</p>
21+
```
22+
23+
## API
24+
25+
### renderContent(markdown, context = {}, options = {})
26+
27+
Render a string of `markdown` with optional `context`. Returns a `Promise`.
28+
29+
Liquid will be looking for includes in `${process.cwd()}/includes`.
30+
31+
Options:
32+
33+
- `encodeEntities`: Encode html entities. Default: `false`.
34+
- `fileName`: File name for debugging purposes.
35+
- `textOnly`: Output text instead of html using [cheerio](https://ghub.io/cheerio).
36+
37+
### .liquid
38+
39+
The [Liquid](https://ghub.io/liquid) instance used internally.
40+
41+
### Code block headers
42+
43+
You can add a header to code blocks by adding the `{:copy}` annotation after the code fences:
44+
45+
```js{:copy}
46+
const copyMe = true
47+
```
48+
49+
This renders:
50+
51+
![image](https://user-images.githubusercontent.com/10660468/95881747-e96c6900-0d46-11eb-9abf-1e8ad16c7646.png)
52+
53+
The un-highlighted text is available as `button.js-btn-copy`'s `data-clipboard-text` attribute.

lib/render-content.js renamed to lib/render-content/index.js

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
const renderContent = require('@github-docs/render-content')
2-
const { ExtendedMarkdown, tags } = require('./liquid-tags/extended-markdown')
1+
const renderContent = require('./renderContent')
2+
const { ExtendedMarkdown, tags } = require('../liquid-tags/extended-markdown')
33

44
// Include custom tags like {% link_with_intro /article/foo %}
5-
renderContent.liquid.registerTag('liquid_tag', require('./liquid-tags/liquid-tag'))
6-
renderContent.liquid.registerTag('link', require('./liquid-tags/link'))
7-
renderContent.liquid.registerTag('link_with_intro', require('./liquid-tags/link-with-intro'))
8-
renderContent.liquid.registerTag('homepage_link_with_intro', require('./liquid-tags/homepage-link-with-intro'))
9-
renderContent.liquid.registerTag('link_in_list', require('./liquid-tags/link-in-list'))
10-
renderContent.liquid.registerTag('topic_link_in_list', require('./liquid-tags/topic-link-in-list'))
11-
renderContent.liquid.registerTag('link_with_short_title', require('./liquid-tags/link-with-short-title'))
12-
renderContent.liquid.registerTag('indented_data_reference', require('./liquid-tags/indented-data-reference'))
13-
renderContent.liquid.registerTag('data', require('./liquid-tags/data'))
14-
renderContent.liquid.registerTag('octicon', require('./liquid-tags/octicon'))
5+
renderContent.liquid.registerTag('liquid_tag', require('../liquid-tags/liquid-tag'))
6+
renderContent.liquid.registerTag('link', require('../liquid-tags/link'))
7+
renderContent.liquid.registerTag('link_with_intro', require('../liquid-tags/link-with-intro'))
8+
renderContent.liquid.registerTag('homepage_link_with_intro', require('../liquid-tags/homepage-link-with-intro'))
9+
renderContent.liquid.registerTag('link_in_list', require('../liquid-tags/link-in-list'))
10+
renderContent.liquid.registerTag('topic_link_in_list', require('../liquid-tags/topic-link-in-list'))
11+
renderContent.liquid.registerTag('link_with_short_title', require('../liquid-tags/link-with-short-title'))
12+
renderContent.liquid.registerTag('indented_data_reference', require('../liquid-tags/indented-data-reference'))
13+
renderContent.liquid.registerTag('data', require('../liquid-tags/data'))
14+
renderContent.liquid.registerTag('octicon', require('../liquid-tags/octicon'))
1515

1616
for (const tag in tags) {
1717
// Register all the extended markdown tags, like {% note %} and {% warning %}

lib/render-content/liquid.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
const Liquid = require('liquid')
2+
const semver = require('semver')
3+
const path = require('path')
4+
const engine = new Liquid.Engine()
5+
engine.registerFileSystem(
6+
new Liquid.LocalFileSystem(path.join(process.cwd(), 'includes'))
7+
)
8+
9+
// GHE versions are not valid SemVer, but can be coerced...
10+
// https://github.com/npm/node-semver#coercion
11+
12+
Liquid.Condition.operators.ver_gt = (cond, left, right) => {
13+
if (!matchesVersionString(left)) return false
14+
if (!matchesVersionString(right)) return false
15+
16+
const [leftPlan, leftRelease] = splitVersion(left)
17+
const [rightPlan, rightRelease] = splitVersion(right)
18+
19+
if (leftPlan !== rightPlan) return false
20+
21+
return semver.gt(semver.coerce(leftRelease), semver.coerce(rightRelease))
22+
}
23+
24+
Liquid.Condition.operators.ver_lt = (cond, left, right) => {
25+
if (!matchesVersionString(left)) return false
26+
if (!matchesVersionString(right)) return false
27+
28+
const [leftPlan, leftRelease] = splitVersion(left)
29+
const [rightPlan, rightRelease] = splitVersion(right)
30+
31+
if (leftPlan !== rightPlan) return false
32+
33+
return semver.lt(semver.coerce(leftRelease), semver.coerce(rightRelease))
34+
}
35+
36+
module.exports = engine
37+
38+
function matchesVersionString (input) {
39+
return input && input.match(/^(?:[a-z](?:[a-z-]*[a-z])?@)?\d+(?:\.\d+)*/)
40+
}
41+
// Support new version formats where version = plan@release
42+
// e.g., [email protected], where enterprise-server is the plan and 2.21 is the release
43+
// e.g., free-pro-team@latest, where free-pro-team is the plan and latest is the release
44+
// in addition to legacy formats where the version passed is simply 2.21
45+
function splitVersion (version) {
46+
// The default plan when working with versions is "enterprise-server".
47+
// Default to that value here to support backward compatibility from before
48+
// plans were explicitly included.
49+
let plan = 'enterprise-server'
50+
let release
51+
52+
const lastIndex = version.lastIndexOf('@')
53+
if (lastIndex === -1) {
54+
release = version
55+
} else {
56+
plan = version.slice(0, lastIndex)
57+
release = version.slice(lastIndex + 1)
58+
}
59+
60+
return [plan, release]
61+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
const h = require('hastscript')
2+
const octicons = require('@primer/octicons')
3+
const parse5 = require('parse5')
4+
const fromParse5 = require('hast-util-from-parse5')
5+
6+
const LANGUAGE_MAP = {
7+
asp: 'ASP',
8+
aspx: 'ASP',
9+
'aspx-vb': 'ASP',
10+
as3: 'ActionScript',
11+
apache: 'ApacheConf',
12+
nasm: 'Assembly',
13+
bat: 'Batchfile',
14+
'c#': 'C#',
15+
csharp: 'C#',
16+
c: 'C',
17+
'c++': 'C++',
18+
cpp: 'C++',
19+
chpl: 'Chapel',
20+
coffee: 'CoffeeScript',
21+
'coffee-script': 'CoffeeScript',
22+
cfm: 'ColdFusion',
23+
'common-lisp': 'Common Lisp',
24+
lisp: 'Common Lisp',
25+
dpatch: 'Darcs Patch',
26+
dart: 'Dart',
27+
elisp: 'Emacs Lisp',
28+
emacs: 'Emacs Lisp',
29+
'emacs-lisp': 'Emacs Lisp',
30+
pot: 'Gettext Catalog',
31+
html: 'HTML',
32+
xhtml: 'HTML',
33+
'html+erb': 'HTML+ERB',
34+
erb: 'HTML+ERB',
35+
irc: 'IRC log',
36+
json: 'JSON',
37+
jsp: 'Java Server Pages',
38+
java: 'Java',
39+
javascript: 'JavaScript',
40+
js: 'JavaScript',
41+
lhs: 'Literate Haskell',
42+
'literate-haskell': 'Literate Haskell',
43+
objc: 'Objective-C',
44+
openedge: 'OpenEdge ABL',
45+
progress: 'OpenEdge ABL',
46+
abl: 'OpenEdge ABL',
47+
pir: 'Parrot Internal Representation',
48+
posh: 'PowerShell',
49+
puppet: 'Puppet',
50+
'pure-data': 'Pure Data',
51+
raw: 'Raw token data',
52+
rb: 'Ruby',
53+
ruby: 'Ruby',
54+
r: 'R',
55+
scheme: 'Scheme',
56+
bash: 'Shell',
57+
sh: 'Shell',
58+
shell: 'Shell',
59+
zsh: 'Shell',
60+
supercollider: 'SuperCollider',
61+
tex: 'TeX',
62+
ts: 'TypeScript',
63+
vim: 'Vim script',
64+
viml: 'Vim script',
65+
rst: 'reStructuredText',
66+
xbm: 'X BitMap',
67+
xpm: 'X PixMap',
68+
yaml: 'YAML',
69+
yml: 'YAML',
70+
71+
// Unofficial languages
72+
shellsession: 'Shell',
73+
jsx: 'JSX'
74+
}
75+
76+
const COPY_REGEX = /\{:copy\}$/
77+
78+
/**
79+
* Adds a bar above code blocks that shows the language and a copy button
80+
*/
81+
module.exports = function addCodeHeader (node) {
82+
// Check if the language matches `lang{:copy}`
83+
const hasCopy = node.lang && COPY_REGEX.test(node.lang)
84+
85+
if (hasCopy) {
86+
// js{:copy} => js
87+
node.lang = node.lang.replace(COPY_REGEX, '')
88+
} else {
89+
// It doesn't have the copy annotation, so don't add the header
90+
return
91+
}
92+
93+
// Display the language using the above map of `{ [shortCode]: language }`
94+
const language = LANGUAGE_MAP[node.lang] || node.lang || 'Code'
95+
96+
const btnIconHtml = octicons.clippy.toSVG()
97+
const btnIconAst = parse5.parse(String(btnIconHtml))
98+
const btnIcon = fromParse5(btnIconAst, btnIconHtml)
99+
100+
// Need to create the header using Markdown AST utilities, to fit
101+
// into the Unified processor ecosystem.
102+
const header = h(
103+
'header',
104+
{
105+
class: [
106+
'd-flex',
107+
'flex-items-center',
108+
'flex-justify-between',
109+
'p-2',
110+
'text-small',
111+
'rounded-top-1',
112+
'border'
113+
]
114+
},
115+
[
116+
h('span', language),
117+
h(
118+
'button',
119+
{
120+
class: [
121+
'js-btn-copy',
122+
'btn',
123+
'btn-sm',
124+
'tooltipped',
125+
'tooltipped-nw'
126+
],
127+
'data-clipboard-text': node.value,
128+
'aria-label': 'Copy code to clipboard'
129+
},
130+
btnIcon
131+
)
132+
]
133+
)
134+
135+
return {
136+
before: [header]
137+
}
138+
}

0 commit comments

Comments
 (0)