The post id 34dead42a28 contained a new paragraph type: H2. Previously
the only known header types were H3 and H4. In this case, the paragraph
doesn't actually get rendered because it's the page title which is
removed from the page nodes (see commits 6baba803 and then fba87c10).
However, it somehow an author is able to get an H2 paragraph into the
page, it will display as an <h1> just as H3 displays as <h2> and H4
displays as <h3>.
This patch adds support for development with the Nix package manager. In
order to support the traditional nix-shell tool as well as the (still
experimental) Nix Flakes feature of the upcoming version of Nix, this
patch adds shell.nix *and* flake.nix/flake.lock. Usage instructions
have been added to the README.
This patch further improves the proposed pattern for the Redirector
extension. In contrast to the old pattern, …
* … it will redirect the URL https://medium.com.
* … it will *not* redirect URLs with top-level domains like mediumXcom.
(This point is purely theoretical, but it makes the regular expression
more correct and consistent.)
* … it will *not* redirect URLs like https://link.medium.com/AXEtCilplkb
which Scribe currently cannot handle. These are shortened URLs that
users get when they use the Twitter button on Medium to share a post.
In order to implement the last point (not matching link.medium.com), the
pattern uses negative lookbehind. This feature of regular expressions is
supported by all recent browsers for which Redirector is available
(Firefox, Chrome, Edge, Opera)[^1], including the current version of
Firefox ESR (Extended Stability Release).
[^1]: https://caniuse.com/js-regexp-lookbehind
In the current redirector example, "scribe.rip" is hardcoded as the
destination. This patch simply changes that to use the app_domain
environment variable, so people wanting to use a community instance
aren't mistakenly redirected to the main scribe.rip instance.
The old pattern matches all host names that end with medium.com. The new
pattern matches only medium.com and its sub-domains. For example, the
old pattern would have matched
https://foomedium.com/@user/post-123456abcdef.
When a post has a gi= query param, Medium makes a global_identifier
"query". This redirects via a 307 temporary redirect to a url that
looks like this:
https://medium.com/m/global-identity?redirectUrl=https%3A%2F%2Fexample.c
om%2Fmy-post-000000000000
Previously, scribe looked for the Medium post id in the url's path, not
it's query params since query params can include other garbage like
medium_utm (not related to medium.com). Now it looks first for the post
id in the path, then looks to the redirectUrl as a fallback.
The subtitle has been removed because it's difficult to find and error
prone to guess at. It is somewhat accessible from the post's
previewContent field in GraphQL but that can be truncated.
Right now this links to the user's medium page. It may link to an
internal page in the future.
Instead of the Page taking the author as a string, it now takes a
PostResponse::Creator object. The Articles::ShowPage then converts the
Creator (a name and user_id) to an author link.
Finally, I did some refactoring of UserAnchor (which I thought I was
going to use for this) to change it's userId attribute to user_id as is
Crystal convention.
In tufte.css blockquotes should contain a <p> that holds the content
and an optional <footer> for the source of the quote. Otherwise the
block quote text is unbounded and is way too wide. This wraps the
content in a paragraph
PostResponse::Paragraph's that are of type IFRAME have extra data in the
iframe attribute to specify what's in the iframe. Not all data is the
same, however. I've identified three types and am using the new
EmbeddedConverter class to convert them:
* EmbeddedContent, the full iframe experience
* GithubGist, because medium or github treat embeds differently for
whatever reason
* EmbeddedLink, the old style, just a link to the content. Effectively
a fallback
The size of the original iframe is also specified as an attribute. This
code resizes it. The resizing is determined by figuring out the
width/height ratio and setting the width to 800.
EmbeddedContent can be displayed if we have an embed.ly url, which most
iframe response data has. GitHub gists are a notable exception. Gists
instead can be embedded simply by taking the gist URL and attaching .js
to the end. That becomes the iframe's src attribute.
The PostResponse::Paragraph's iframe attribute is nillable. Previous
code used lots of if-statements with variable bindings to work with the
possible nil values:
```crystal
if foo = obj.nillable_value
# obj.nillable_value was not nil and foo contains the value
else
# obj.nillable_value was nil so do something else
end
```
See https://crystal-lang.org/reference/syntax_and_semantics/if_var.html
for more info
In the EmbeddedConverter the monads library has been introduced to get
rid of at least one level of nillability. This wraps values in Maybe
which allows for a cleaner interface:
```crystal
Monads::Try(Value).new(->{ obj.nillable_value })
.to_maybe
.fmap(->(value: Value) { # do something with value })
.value_or(# value was nil, do something else)
```
This worked to get the iframe attribute from a Paragraph:
```crystal
Monads::Try(PostResponse::IFrame).new(->{ paragraph.iframe })
.to_maybe
.fmap(->(iframe : PostResponse::IFrame) { # iframe is not nil! })
.fmap(#and so on)
.value_or(Empty.new)
```
iframe only has one attribute: mediaResource which contains the iframe
data. That was used to determine one of the three types above.
Finally, Tufte.css has options for iframes. They mostly look good except
for tweets which are too small and weirdly in the center of the page
which actually looks off-center. That's for another day though.
On a thin viewport like a phone these show up as hidden at first until
the user expands them by interacting with the "writing hand" icon. Each
margin note needs a small bit of markup near it to enable the toggle.
Each also needs a unique ID to ensure it doesn't interact with
alternate content. The `hash` value of the FigureCaption's `children`
provides this unique value.
Also wrap the content in an article for semantic formatting
tufte.css requires that content is wrapped in an <article> and at least
one <section>. There's no way of determining new semantic sections so
there is only one.
Medium guides each post to have a Title and Subtitle. They are rendered
as the first two paragraphs: H3 and H4 respectively. If they exist, a
new PageConverter class extracts them and sets them on the page.
However, they aren't required. If the first two paragraphs aren't H3
and H4, the PageConverter falls back to using the first paragraph as
the title, and setting the subtitle to blank.
The remaining paragraphs are passed into the ParagraphConverter as
normal.
General CSS hygiene dictates that you shouldn't go beyond an H3 tag. H1
for the document title, H2 for section headings, and H3 for low-level
headings.
The CSS itself will take care of scaling the image height based on the
width. We still need to know the height to fetch the image because the
height is in the URL, but we don't need to render it in the HTML.
Example:
* Text: "strong and emphasized only"
* Markups:
* Strong: 0..10
* Emphasis: 7..21
First, get all the borders of the markups, including the start (0) and
end (text.size) indexes of the text in order:
```
[0, 7, 10, 21, 26]
```
Then attach markups to each range. Note that the ranges are exclusive;
they don't include the final number:
* 0...7: Strong
* 7...10: Strong, Emphasized
* 10...21: Emphasized
* 21...26: N/A
Bundle each range and it's related markups into a value object
RangeWithMarkup and return the list.
Loop through that list and recursively apply each markup to each
segment of text:
* Apply a `Strong` markup to the text "strong "
* Apply a `Strong` markup to the text "and"
* Wrap that in an `Emphasis` markup
* Apply an `Emphasis` markup to the text " emphasized"
* Leave the text " only" as is
---
This has the side effect of breaking up the nodes more than they need
to be broken up. For example right now the algorithm creates this HTML:
```
<strong>strong </strong><em><strong>and</strong></em>
```
instead of:
```
<strong>strong <em>and</em></strong>
```
But that's a task for another day.
The impetus for this change was to help make the MarkupConverter code
more robust. However, it's also possible that an Anchor can contain
styled text. For example, in markdown someone might write a link that
contains some <strong> text:
```markdown
[this link is so **good**](https://example.com)
```
This setup will now allow that. Unknown if UserAnchor can ever contain
any text that isn't just the user's name, but it's easy to deal with
and makes the typing much easier.
To enable the crystal formatting, the extension saves the crystal path
to a .nova folder. These paths are specific to my computer so I don't
need to store them in the repo
Instead of showing only: Click to visit embedded content
An embedded link now displays with the domain it's linking to: Embedded
content at example.com
This hopefully breaks up the links a bit so it'e easier to distinguish
between a bunch of them in a row (as long as they are on different
domains).
Instead of getting the full size image, the image can be fetched with a
width and height parameter so that only the resized data is
transferred. The url looks like this:
https://cdn-images-1.medium.com/fit/c/<width>/<height>/<media-id>
I picked a max image width of 800px. If the image width is more than
that, it scales the width down to 800, then applies that ratio to the
height. If it's smaller than that, the image is displayed as the
original.