Skip to content

Commit 405b2ad

Browse files
Merge #869
869: Show Contents of Readme on Crate Pages r=carols10cents Hey everyone, I started working on README rendering earlier and I thought that I should get some feedback now that it is working correctly on my local instance. I edited the `/crates/new` endpoint so that when a crate is uploaded to the registry, its README (markdown only) is rendered to HTML and a `readmes/{crate}/{crate}-{version}.html` file is created on S3 when the crate file is uploaded there. Then, when visiting a crate page, the Ember application makes a request to `{s3}/readmes/{crate}/{crate}-{version}.html`: if the file is found, its content is rendered in place, if not nothing happens. This way, the renderd README is cached and versioned, following @ashleygwilliams [recommendations](#81 (comment)). On the `crate/version` route, the README contents is displayed in place of the package description. The generated HTML is sanitized using [Ammonia](https://crates.io/crates/ammonia). This crate removes all tags and attributes that are not whitelisted, making the resulting markup as safe as possible for the end user. Bonus point: is uses the html5ever crate (created by the Servo project) to parse HTML. ~~~Codeblocks syntax highlighting is done server-side using [syntect](https://crates.io/crates/syntect). This crate uses Sublime syntax files to highlight code samples, which means that we automatically have access to a lot of pre-existing grammars and we can easily add/create missing ones.~~~ Syntax highlighting is now done client-side using PrismJS due to syntect's dependency onigumura conflicting with git2. Finally, I added a new script in `src/bin` named `render-readmes`. Its goal is to render the README of every uploaded crate version and to upload it on S3. This means that if modify the renderer behavior and we want to retroactively apply it, we're just one command (and a lil' bit of time) away ! (Though I might have left too much `unwrap()`/`expect()` in it) TODO: * [x] Security checks in `render::markdown_to_html` (script tag, javascript callbacks, more ?) * [x] Tests * [x] Documentation * [x] ~Edit delete-crate & delete-version scripts to delete README files~ It would seem that the scripts only delete database entries, not stored files * [x] Script to render published crates README (?) Fixes #81 Preview: ![screenshot from 2017-07-10 00-22-45](https://user-images.githubusercontent.com/1275463/27998080-f83bad7a-6505-11e7-884e-95ee4f7c2a47.png)
2 parents 9ce04b1 + e28e48f commit 405b2ad

22 files changed

+1246
-305
lines changed

Diff for: Cargo.lock

+235-21
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ git2 = "0.6.4"
2727
flate2 = "0.2"
2828
semver = "0.5"
2929
url = "1.2.1"
30+
tar = "0.4.13"
3031

3132
r2d2 = "0.7.0"
3233
openssl = "0.9.9"
@@ -47,6 +48,8 @@ serde_derive = "1.0.0"
4748
serde = "1.0.0"
4849
clippy = { version = "=0.0.142", optional = true }
4950
chrono = "0.4.0"
51+
pulldown-cmark = { version = "0.0.15", default-features = false }
52+
ammonia = "0.5.0"
5053

5154
conduit = "0.8"
5255
conduit-conditional-get = "0.8"

Diff for: app/components/crate-readme.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Ember from 'ember';
2+
3+
export default Ember.Component.extend({
4+
rendered: '',
5+
didRender() {
6+
this._super(...arguments);
7+
this.$('pre > code').each(function() {
8+
window.Prism.highlightElement(this);
9+
});
10+
}
11+
});

Diff for: app/models/version.js

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import DS from 'ember-data';
44
export default DS.Model.extend({
55
num: DS.attr('string'),
66
dl_path: DS.attr('string'),
7+
readme_path: DS.attr('string'),
78
created_at: DS.attr('date'),
89
updated_at: DS.attr('date'),
910
downloads: DS.attr('number'),

Diff for: app/routes/crate/version.js

+20-1
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,28 @@ export default Route.extend({
9999
`Version '${params.version_num}' of crate '${crate.get('name')}' does not exist`);
100100
}
101101

102-
return version ||
102+
const result = version ||
103103
versions.find(version => version.get('num') === maxVersion) ||
104104
versions.objectAt(0);
105+
if (result.get('readme_path')) {
106+
this.get('ajax').request(result.get('readme_path'))
107+
.then((r) => this.get('ajax').raw(r.url, {
108+
method: 'GET',
109+
dataType: 'html',
110+
headers: {
111+
// We need to force the Accept header, otherwise crates.io won't return
112+
// the readme file when not using S3.
113+
Accept: '*/*',
114+
},
115+
}))
116+
.then((r) => {
117+
crate.set('readme', r.payload);
118+
})
119+
.catch(() => {
120+
crate.set('readme', null);
121+
});
122+
}
123+
return result;
105124
});
106125
},
107126

Diff for: app/styles/crate.scss

+8
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@
208208
.docs {
209209
@include flex(7);
210210
padding-right: 40px;
211+
max-width: 600px;
211212
}
212213
.authorship {
213214
@include flex(3);
@@ -302,6 +303,13 @@
302303
color: red;
303304
}
304305
}
306+
.crate-readme {
307+
line-height: 1.5;
308+
309+
pre {
310+
overflow-x: scroll;
311+
}
312+
}
305313
.last-update {
306314
color: $main-color-light;
307315
font-size: 90%;

Diff for: app/templates/components/crate-readme.hbs

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{{{rendered}}}

Diff for: app/templates/crate/version.hbs

+69-89
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,28 @@
2828
<div class="quick-links">
2929
<ul>
3030
{{#if crate.homepage}}
31-
<li><a href="{{crate.homepage}}">Homepage</a></li>
31+
<li><a href="{{crate.homepage}}">Homepage</a></li>
32+
{{/if}}
33+
{{#if crate.wiki}}
34+
<li><a href="{{crate.wiki}}">Wiki</a></li>
35+
{{/if}}
36+
{{#if crate.mailing_list}}
37+
<li><a href="{{crate.mailing_list}}">Mailing list</a></li>
3238
{{/if}}
3339
{{#if crate.documentation}}
34-
<li><a href="{{crate.documentation}}">Documentation</a></li>
40+
<li><a href="{{crate.documentation}}">Documentation</a></li>
3541
{{/if}}
3642
{{#if crate.repository}}
37-
<li><a href="{{crate.repository}}">Repository</a></li>
43+
<li><a href="{{crate.repository}}">Repository</a></li>
44+
{{/if}}
45+
{{#if crate.reverse_dependencies}}
46+
<li>
47+
{{#link-to 'crate.reverse_dependencies' (query-params dependency=crate.crate_id)}}
48+
Dependent crates
49+
{{/link-to}}
50+
</li>
51+
{{else}}
52+
<li>No dependent crates</li>
3853
{{/if}}
3954
</ul>
4055
</div>
@@ -54,12 +69,6 @@
5469
{{else}}
5570
<div class='crate-info'>
5671
<div class='docs'>
57-
{{#if crate.description}}
58-
<div class='about'>
59-
<h3>About This Package</h3>
60-
<p>{{ crate.description }}</p>
61-
</div>
62-
{{/if}}
6372
<div class='install'>
6473
<div class='action'>Cargo.toml</div>
6574
<code id="crate-toml">{{ crate.name }} = "{{ currentVersion.num }}"</code>
@@ -82,6 +91,11 @@
8291
{{/if}}
8392
</span>
8493
</div>
94+
{{#if crate.readme}}
95+
<div class="crate-readme">
96+
{{crate-readme rendered=crate.readme}}
97+
</div>
98+
{{/if}}
8599
</div>
86100
<div class='authorship'>
87101
<div class='top'>
@@ -166,88 +180,54 @@
166180
{{/each}}
167181
</ul>
168182
</div>
169-
</div>
170-
</div>
171-
</div>
172183

173-
<div id='crate-links'>
174-
{{#if anyLinks}}
175-
<div class='section'>
176-
<h3>Links</h3>
177-
<ul>
178-
{{#if crate.homepage}}
179-
<li><a href="{{crate.homepage}}">Homepage</a></li>
180-
{{/if}}
181-
{{#if crate.wiki}}
182-
<li><a href="{{crate.wiki}}">Wiki</a></li>
183-
{{/if}}
184-
{{#if crate.mailing_list}}
185-
<li><a href="{{crate.mailing_list}}">Mailing list</a></li>
186-
{{/if}}
187-
{{#if crate.documentation}}
188-
<li><a href="{{crate.documentation}}">Documentation</a></li>
189-
{{/if}}
190-
{{#if crate.repository}}
191-
<li><a href="{{crate.repository}}">Repository</a></li>
192-
{{/if}}
193-
{{#if crate.reverse_dependencies}}
194-
<li>
195-
{{#link-to 'crate.reverse_dependencies' (query-params dependency=crate.crate_id)}}
196-
Dependent crates
197-
{{/link-to}}
198-
</li>
199-
{{else}}
200-
<li>No dependent crates</li>
201-
{{/if}}
202-
</ul>
203-
</div>
204-
{{/if}}
184+
<div class='section' id='crate-versions'>
185+
<h3>Versions</h3>
186+
<ul>
187+
{{#each smallSortedVersions as |version|}}
188+
<li>
189+
{{#link-to 'crate.version' version.num}}
190+
{{ version.num }}
191+
{{/link-to}}
192+
<span class='date'>{{moment-format version.created_at 'll'}}</span>
193+
{{#if version.yanked}}
194+
<span class='yanked'>yanked</span>
195+
{{/if}}
196+
</li>
197+
{{/each}}
198+
</ul>
199+
<span class='small'>
200+
{{#if hasMoreVersions}}
201+
{{#link-to 'crate.versions' crate}}
202+
show all {{ crate.versions.length }} versions
203+
{{/link-to}}
204+
{{/if}}
205+
</span>
206+
</div>
205207

206-
<div class='section' id='crate-dependencies'>
207-
<h3>Dependencies</h3>
208-
<ul>
209-
{{#each currentDependencies as |dep|}}
210-
{{link-to-dep dep=dep}}
211-
{{else}}
212-
<li>None</li>
213-
{{/each}}
214-
</ul>
215-
</div>
208+
<div class='section' id='crate-dependencies'>
209+
<h3>Dependencies</h3>
210+
<ul>
211+
{{#each currentDependencies as |dep|}}
212+
{{link-to-dep dep=dep}}
213+
{{else}}
214+
<li>None</li>
215+
{{/each}}
216+
</ul>
217+
</div>
216218

217-
{{#if currentDevDependencies}}
218-
<div class='section' id='crate-dev-dependencies'>
219-
<h3>Dev-Dependencies</h3>
220-
<ul>
221-
{{#each currentDevDependencies as |dep|}}
222-
{{link-to-dep dep=dep}}
223-
{{/each}}
224-
</ul>
219+
{{#if currentDevDependencies}}
220+
<div class='section' id='crate-dev-dependencies'>
221+
<h3>Dev-Dependencies</h3>
222+
<ul>
223+
{{#each currentDevDependencies as |dep|}}
224+
{{link-to-dep dep=dep}}
225+
{{/each}}
226+
</ul>
227+
</div>
228+
{{/if}}
229+
</div>
225230
</div>
226-
{{/if}}
227-
228-
<div class='section' id='crate-versions'>
229-
<h3>Versions</h3>
230-
<ul>
231-
{{#each smallSortedVersions as |version|}}
232-
<li>
233-
{{#link-to 'crate.version' version.num}}
234-
{{ version.num }}
235-
{{/link-to}}
236-
<span class='date'>{{moment-format version.created_at 'll'}}</span>
237-
{{#if version.yanked}}
238-
<span class='yanked'>yanked</span>
239-
{{/if}}
240-
</li>
241-
{{/each}}
242-
</ul>
243-
<span class='small'>
244-
{{#if hasMoreVersions}}
245-
{{#link-to 'crate.versions' crate}}
246-
show all {{ crate.versions.length }} versions
247-
{{/link-to}}
248-
{{/if}}
249-
</span>
250-
</div>
251231
</div>
252232

253233
<div id='crate-downloads'>
@@ -256,14 +236,14 @@
256236
<div class='stat'>
257237
<span class='num'>
258238
{{svg-jar "download"}}
259-
{{ format-num downloadsContext.downloads }}
239+
{{format-num downloadsContext.downloads}}
260240
</span>
261241
<span class='desc small'>Downloads all time</span>
262242
</div>
263243
<div class='stat'>
264244
<span class="{{if crate.versions.isPending 'loading'}} num">
265245
{{svg-jar "crate"}}
266-
{{ crate.versions.length }}
246+
{{crate.versions.length}}
267247
</span>
268248
<span class='desc small'>Versions published</span>
269249
</div>

Diff for: ember-cli-build.js

+21
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,31 @@
55
const EmberApp = require('ember-cli/lib/broccoli/ember-app');
66

77
module.exports = function(defaults) {
8+
const highlightedLanguages = [
9+
'bash',
10+
'clike',
11+
'glsl',
12+
'go',
13+
'ini',
14+
'javascript',
15+
'json',
16+
'markup',
17+
'protobuf',
18+
'ruby',
19+
'rust',
20+
'scss',
21+
'sql',
22+
'yaml'
23+
];
24+
825
let app = new EmberApp(defaults, {
926
babel6: {
1027
plugins: ['transform-object-rest-spread'],
1128
},
29+
'ember-prism': {
30+
theme: 'twilight',
31+
components: highlightedLanguages,
32+
}
1233
});
1334

1435
// Use `app.import` to add additional libraries to the generated

0 commit comments

Comments
 (0)