After getting the user's input, we need to figure out a filename to write the file out as. We can either use the title the user passed in, or we can try to use heuristics to find a title from the markdown content.
We can combine the user's title from the cli
arguments (an Option
) with the logic we
use as a heuristic for the markdown (also
an Option
).
// use `title` if the user passed it in,
// otherwise try to find a heading in the markdown
let document_title = title.or_else(|| {
contents
.lines()
.find(|v| v.starts_with("# "))
// markdown headings are required to have `# ` with
// at least one space
.map(|maybe_line| maybe_line.trim_start_matches("# ").to_string())
});
option.or_else()
returns the original
option if it is a Some
value, or executes
the function to return a different Option
if not. So document_title
is an
Option<String>
either way and we've
covered all of the possible scenarios:
Which leaves us to our markdown heuristic.
Markdown headings are required to have #
with at least one space, so we can turn
contents
into an iterator and find
the
first line that starts with #
.
If we find one, we want to trim #
off of
the heading to get just the heading content,
so we can use map
to operate on the value
inside of the Option
returned by find
if
it exists.
contents
.lines()
.find(|v| v.starts_with("# "))
.map(|maybe_line| maybe_line.trim_start_matches("# ").to_string())
Now this code works fine, but it exposes a bug in our file handling.
If we write_all
to the file
, that moves the
internal cursor to the end of that content. So
when we run our code now, contents
is
skipping the first #
bytes, which means our
heuristic will only find the second heading in
the file.
To fix this, we can bring in the std::io::Seek
trait, and seek to the beginning of the file.
let mut contents = String::new();
file.seek(SeekFrom::Start(0))?;
file.read_to_string(&mut contents)?;
Instructor: [0:00] After getting the user's input, we need to figure out a file name to write the file out as. We can either use the title the user passed in, or we can try to use some heuristics to find a title from the Markdown content they just wrote.
[0:13] We can combine the user's title from the CLI arguments, which they may or may not have passed in and is thus an option type, with the logic we use as a heuristic for the Markdown, which will also be an option. We may or may not be able to get an answer out of it.
[0:30] Title is the first optional type. It's the one the user passed in or didn't pass in to the CLI. The or_else function on options will return the original option if it's a sum value or execute the closure to return a different option if it's none.
[0:48] Document title is an option string, as rust-analyzer is telling us here. Either way, we've covered all of the possible scenarios. If the user passes in a title, we get Title. If the user has written content with a heading, we use the internals of this closure to find that heading and return it.
[1:10] If the user does neither of these, Document title will be None, which leaves us to talk about our Markdown heuristic. We can take the contents of the file that we just read to a string and turn it into an iterator over lines.
[1:25] So you don't have to read the Markdown spec, I'll tell you that in the Markdown spec ATX headings have to start with a hashtag, or an octothorpe, followed by at least one space. This is not true for all possible headings that you can put into a Markdown file, but it's true for the primary heading if it's in ATX format. That's what makes this a heuristic.
[1:47] Contents .lines give us an iterator, which we're using .find on, which gives us a line. On each line we test to see if it starts with the hash and a space. If it does, then we use .map to map over that.
[2:04] Find returns us an option. Map will let us operate on the value inside of the sum if there is one. If it's None, this map will never execute. Given a line, we'll want to trim off the starting hash and then the space.
[2:22] We want to match the type of the title, which is a String, so we use ToString because we can see the trim-start matches gives us a string slice. We need to convert that into an owned string, or a String. Let's give it a go.
[2:41] We can see our Options got debugged out. We can start writing a heading, some content, save, exit the file. Our program crashes as expected because we still have to-do at the end. We can see that the file contents are here and the document title is None, but it shouldn't be None. It should be a heading.
[3:05] If we look at the contents of file here, we can notice a bug. When we wrote to the file with write_all using our template, it seeked the internal cursor of the file forward two bytes, which means that when we read to string, we're reading from a two-byte offset into the file because we haven't reset that seek. Thus, we don't get the initial half for our heading.
[3:33] To fix this, we can seek on the file right before we try to read the whole thing. Note that this will require bringing SeekFrom into scope, which will seek us from the start of . We also need to bring in Seek trait, which is what will allow us to use this Seek function on the file.
[3:56] Without bringing in Seek, we won't get the Seek function. Without bringing in SeekFrom, we can't construct this start value. Now we can see that we've seeked for the beginning of the file, we've debugged out the entire contents of the file, and our document title gives us some a heading.
yeah! it's most likely the fact that the editor you're using is actually moving the file and creating a totally new file when you save. vim is known to do this in some cases, for example, with it's filename~
style files.
You can check this by using dbg! on the file
before and after the user edits it. If that's the case for you, you can use fs::read instead of using the file returned from the builder.
Thanks, Chris!
I have problems getting this to work :( If I run the following code:
The contents is always
"# "
. I already added the seek call to the code above. Can you tell me what the mistake is?