Skip to content

Commit 3755fe9

Browse files
committed
Auto merge of #44781 - QuietMisdreavus:doc-include, r=GuillaumeGomez
rustdoc: include external files in documentation (RFC 1990) Part of rust-lang/rfcs#1990 (needs work on the error reporting, which i'm deferring to after this initial PR) cc #44732 Also fixes #42760, because the prep work for the error reporting made it easy to fix that at the same time.
2 parents 96e9cee + 52ee203 commit 3755fe9

File tree

17 files changed

+486
-29
lines changed

17 files changed

+486
-29
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# `external_doc`
2+
3+
The tracking issue for this feature is: [#44732]
4+
5+
The `external_doc` feature allows the use of the `include` parameter to the `#[doc]` attribute, to
6+
include external files in documentation. Use the attribute in place of, or in addition to, regular
7+
doc comments and `#[doc]` attributes, and `rustdoc` will load the given file when it renders
8+
documentation for your crate.
9+
10+
With the following files in the same directory:
11+
12+
`external-doc.md`:
13+
14+
```markdown
15+
# My Awesome Type
16+
17+
This is the documentation for this spectacular type.
18+
```
19+
20+
`lib.rs`:
21+
22+
```no_run (needs-external-files)
23+
#![feature(external_doc)]
24+
25+
#[doc(include = "external-doc.md")]
26+
pub struct MyAwesomeType;
27+
```
28+
29+
`rustdoc` will load the file `external-doc.md` and use it as the documentation for the `MyAwesomeType`
30+
struct.
31+
32+
When locating files, `rustdoc` will base paths in the `src/` directory, as if they were alongside the
33+
`lib.rs` for your crate. So if you want a `docs/` folder to live alongside the `src/` directory,
34+
start your paths with `../docs/` for `rustdoc` to properly find the file.
35+
36+
This feature was proposed in [RFC #1990] and initially implemented in PR [#44781].
37+
38+
[#44732]: https://github.com/rust-lang/rust/issues/44732
39+
[RFC #1990]: https://github.com/rust-lang/rfcs/pull/1990
40+
[#44781]: https://github.com/rust-lang/rust/pull/44781

src/librustdoc/clean/mod.rs

+140-4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ use rustc::hir;
4444

4545
use rustc_const_math::ConstInt;
4646
use std::{mem, slice, vec};
47+
use std::iter::FromIterator;
4748
use std::path::PathBuf;
4849
use std::rc::Rc;
4950
use std::sync::Arc;
@@ -300,6 +301,11 @@ impl Item {
300301
pub fn doc_value<'a>(&'a self) -> Option<&'a str> {
301302
self.attrs.doc_value()
302303
}
304+
/// Finds all `doc` attributes as NameValues and returns their corresponding values, joined
305+
/// with newlines.
306+
pub fn collapsed_doc_value(&self) -> Option<String> {
307+
self.attrs.collapsed_doc_value()
308+
}
303309
pub fn is_crate(&self) -> bool {
304310
match self.inner {
305311
StrippedItem(box ModuleItem(Module { is_crate: true, ..})) |
@@ -564,9 +570,69 @@ impl<I: IntoIterator<Item=ast::NestedMetaItem>> NestedAttributesExt for I {
564570
}
565571
}
566572

573+
/// A portion of documentation, extracted from a `#[doc]` attribute.
574+
///
575+
/// Each variant contains the line number within the complete doc-comment where the fragment
576+
/// starts, as well as the Span where the corresponding doc comment or attribute is located.
577+
///
578+
/// Included files are kept separate from inline doc comments so that proper line-number
579+
/// information can be given when a doctest fails. Sugared doc comments and "raw" doc comments are
580+
/// kept separate because of issue #42760.
581+
#[derive(Clone, RustcEncodable, RustcDecodable, PartialEq, Debug)]
582+
pub enum DocFragment {
583+
// FIXME #44229 (misdreavus): sugared and raw doc comments can be brought back together once
584+
// hoedown is completely removed from rustdoc.
585+
/// A doc fragment created from a `///` or `//!` doc comment.
586+
SugaredDoc(usize, syntax_pos::Span, String),
587+
/// A doc fragment created from a "raw" `#[doc=""]` attribute.
588+
RawDoc(usize, syntax_pos::Span, String),
589+
/// A doc fragment created from a `#[doc(include="filename")]` attribute. Contains both the
590+
/// given filename and the file contents.
591+
Include(usize, syntax_pos::Span, String, String),
592+
}
593+
594+
impl DocFragment {
595+
pub fn as_str(&self) -> &str {
596+
match *self {
597+
DocFragment::SugaredDoc(_, _, ref s) => &s[..],
598+
DocFragment::RawDoc(_, _, ref s) => &s[..],
599+
DocFragment::Include(_, _, _, ref s) => &s[..],
600+
}
601+
}
602+
603+
pub fn span(&self) -> syntax_pos::Span {
604+
match *self {
605+
DocFragment::SugaredDoc(_, span, _) |
606+
DocFragment::RawDoc(_, span, _) |
607+
DocFragment::Include(_, span, _, _) => span,
608+
}
609+
}
610+
}
611+
612+
impl<'a> FromIterator<&'a DocFragment> for String {
613+
fn from_iter<T>(iter: T) -> Self
614+
where
615+
T: IntoIterator<Item = &'a DocFragment>
616+
{
617+
iter.into_iter().fold(String::new(), |mut acc, frag| {
618+
if !acc.is_empty() {
619+
acc.push('\n');
620+
}
621+
match *frag {
622+
DocFragment::SugaredDoc(_, _, ref docs)
623+
| DocFragment::RawDoc(_, _, ref docs)
624+
| DocFragment::Include(_, _, _, ref docs) =>
625+
acc.push_str(docs),
626+
}
627+
628+
acc
629+
})
630+
}
631+
}
632+
567633
#[derive(Clone, RustcEncodable, RustcDecodable, PartialEq, Debug, Default)]
568634
pub struct Attributes {
569-
pub doc_strings: Vec<String>,
635+
pub doc_strings: Vec<DocFragment>,
570636
pub other_attrs: Vec<ast::Attribute>,
571637
pub cfg: Option<Rc<Cfg>>,
572638
pub span: Option<syntax_pos::Span>,
@@ -596,6 +662,47 @@ impl Attributes {
596662
None
597663
}
598664

665+
/// Reads a `MetaItem` from within an attribute, looks for whether it is a
666+
/// `#[doc(include="file")]`, and returns the filename and contents of the file as loaded from
667+
/// its expansion.
668+
fn extract_include(mi: &ast::MetaItem)
669+
-> Option<(String, String)>
670+
{
671+
mi.meta_item_list().and_then(|list| {
672+
for meta in list {
673+
if meta.check_name("include") {
674+
// the actual compiled `#[doc(include="filename")]` gets expanded to
675+
// `#[doc(include(file="filename", contents="file contents")]` so we need to
676+
// look for that instead
677+
return meta.meta_item_list().and_then(|list| {
678+
let mut filename: Option<String> = None;
679+
let mut contents: Option<String> = None;
680+
681+
for it in list {
682+
if it.check_name("file") {
683+
if let Some(name) = it.value_str() {
684+
filename = Some(name.to_string());
685+
}
686+
} else if it.check_name("contents") {
687+
if let Some(docs) = it.value_str() {
688+
contents = Some(docs.to_string());
689+
}
690+
}
691+
}
692+
693+
if let (Some(filename), Some(contents)) = (filename, contents) {
694+
Some((filename, contents))
695+
} else {
696+
None
697+
}
698+
});
699+
}
700+
}
701+
702+
None
703+
})
704+
}
705+
599706
pub fn has_doc_flag(&self, flag: &str) -> bool {
600707
for attr in &self.other_attrs {
601708
if !attr.check_name("doc") { continue; }
@@ -610,18 +717,29 @@ impl Attributes {
610717
false
611718
}
612719

613-
pub fn from_ast(diagnostic: &::errors::Handler, attrs: &[ast::Attribute]) -> Attributes {
720+
pub fn from_ast(diagnostic: &::errors::Handler,
721+
attrs: &[ast::Attribute]) -> Attributes {
614722
let mut doc_strings = vec![];
615723
let mut sp = None;
616724
let mut cfg = Cfg::True;
725+
let mut doc_line = 0;
617726

618727
let other_attrs = attrs.iter().filter_map(|attr| {
619728
attr.with_desugared_doc(|attr| {
620729
if attr.check_name("doc") {
621730
if let Some(mi) = attr.meta() {
622731
if let Some(value) = mi.value_str() {
623732
// Extracted #[doc = "..."]
624-
doc_strings.push(value.to_string());
733+
let value = value.to_string();
734+
let line = doc_line;
735+
doc_line += value.lines().count();
736+
737+
if attr.is_sugared_doc {
738+
doc_strings.push(DocFragment::SugaredDoc(line, attr.span, value));
739+
} else {
740+
doc_strings.push(DocFragment::RawDoc(line, attr.span, value));
741+
}
742+
625743
if sp.is_none() {
626744
sp = Some(attr.span);
627745
}
@@ -633,6 +751,14 @@ impl Attributes {
633751
Err(e) => diagnostic.span_err(e.span, e.msg),
634752
}
635753
return None;
754+
} else if let Some((filename, contents)) = Attributes::extract_include(&mi)
755+
{
756+
let line = doc_line;
757+
doc_line += contents.lines().count();
758+
doc_strings.push(DocFragment::Include(line,
759+
attr.span,
760+
filename,
761+
contents));
636762
}
637763
}
638764
}
@@ -650,7 +776,17 @@ impl Attributes {
650776
/// Finds the `doc` attribute as a NameValue and returns the corresponding
651777
/// value found.
652778
pub fn doc_value<'a>(&'a self) -> Option<&'a str> {
653-
self.doc_strings.first().map(|s| &s[..])
779+
self.doc_strings.first().map(|s| s.as_str())
780+
}
781+
782+
/// Finds all `doc` attributes as NameValues and returns their corresponding values, joined
783+
/// with newlines.
784+
pub fn collapsed_doc_value(&self) -> Option<String> {
785+
if !self.doc_strings.is_empty() {
786+
Some(self.doc_strings.iter().collect())
787+
} else {
788+
None
789+
}
654790
}
655791
}
656792

src/librustdoc/html/render.rs

+26-4
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ pub use self::ExternalLocation::*;
3636

3737
#[cfg(stage0)]
3838
use std::ascii::AsciiExt;
39+
use std::borrow::Cow;
3940
use std::cell::RefCell;
4041
use std::cmp::Ordering;
4142
use std::collections::{BTreeMap, HashSet};
@@ -143,6 +144,23 @@ impl SharedContext {
143144
}
144145
}
145146

147+
impl SharedContext {
148+
/// Returns whether the `collapse-docs` pass was run on this crate.
149+
pub fn was_collapsed(&self) -> bool {
150+
self.passes.contains("collapse-docs")
151+
}
152+
153+
/// Based on whether the `collapse-docs` pass was run, return either the `doc_value` or the
154+
/// `collapsed_doc_value` of the given item.
155+
pub fn maybe_collapsed_doc_value<'a>(&self, item: &'a clean::Item) -> Option<Cow<'a, str>> {
156+
if self.was_collapsed() {
157+
item.collapsed_doc_value().map(|s| s.into())
158+
} else {
159+
item.doc_value().map(|s| s.into())
160+
}
161+
}
162+
}
163+
146164
/// Indicates where an external crate can be found.
147165
pub enum ExternalLocation {
148166
/// Remote URL root of the external crate
@@ -1817,6 +1835,9 @@ fn plain_summary_line(s: Option<&str>) -> String {
18171835
}
18181836

18191837
fn document(w: &mut fmt::Formatter, cx: &Context, item: &clean::Item) -> fmt::Result {
1838+
if let Some(ref name) = item.name {
1839+
info!("Documenting {}", name);
1840+
}
18201841
document_stability(w, cx, item)?;
18211842
let prefix = render_assoc_const_value(item);
18221843
document_full(w, item, cx, &prefix)?;
@@ -1893,8 +1914,9 @@ fn render_assoc_const_value(item: &clean::Item) -> String {
18931914

18941915
fn document_full(w: &mut fmt::Formatter, item: &clean::Item,
18951916
cx: &Context, prefix: &str) -> fmt::Result {
1896-
if let Some(s) = item.doc_value() {
1897-
render_markdown(w, s, item.source.clone(), cx.render_type, prefix, &cx.shared)?;
1917+
if let Some(s) = cx.shared.maybe_collapsed_doc_value(item) {
1918+
debug!("Doc block: =====\n{}\n=====", s);
1919+
render_markdown(w, &*s, item.source.clone(), cx.render_type, prefix, &cx.shared)?;
18981920
} else if !prefix.is_empty() {
18991921
write!(w, "<div class='docblock'>{}</div>", prefix)?;
19001922
}
@@ -3326,8 +3348,8 @@ fn render_impl(w: &mut fmt::Formatter, cx: &Context, i: &Impl, link: AssocItemLi
33263348
}
33273349
write!(w, "</span>")?;
33283350
write!(w, "</h3>\n")?;
3329-
if let Some(ref dox) = i.impl_item.doc_value() {
3330-
write!(w, "<div class='docblock'>{}</div>", Markdown(dox, cx.render_type))?;
3351+
if let Some(ref dox) = cx.shared.maybe_collapsed_doc_value(&i.impl_item) {
3352+
write!(w, "<div class='docblock'>{}</div>", Markdown(&*dox, cx.render_type))?;
33313353
}
33323354
}
33333355

0 commit comments

Comments
 (0)