jekyll-devto 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +19 -3
- data/lib/jekyll/devto/feed.xml +17 -2
- data/lib/jekyll/devto/html.rb +86 -10
- data/lib/jekyll/devto/publisher.rb +89 -3
- data/lib/jekyll/devto/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1aa5e1f448c0e19e840473d8829de5c76d9db2f9f27c9e919243086d07d2b7f9
|
|
4
|
+
data.tar.gz: 9abb009ac797c5009074a1120fca05c0fd15771a7404afa4b3f3ea44f53f4574
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3557c4a83b0c514893f5acbce8f0ff78fecab9c2139d02a06c65bbfd5299d2324c8d2f88d83a432358a58d1ff93e7accbb941d552e0421364088d052af4d80a8
|
|
7
|
+
data.tar.gz: 25efecb7281df14f9aadb89cf25a756aba07fa99dc1f73b07b63e7ee401bb0e7f155367f91002a9efd26ed334f40433dc3e2af70005a6440e07fe4db847dba65
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.0](https://github.com/JuanVqz/jekyll-devto/compare/v0.1.0...v0.2.0) (2026-10-02)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* choose dev.to tags and series from the post's front matter ([#13](https://github.com/JuanVqz/jekyll-devto/issues/13)) ([1aa48f7](https://github.com/JuanVqz/jekyll-devto/commit/1aa48f7a362781f660a54d6e041f043db5a11eb8))
|
|
9
|
+
* **html:** bring code block languages to dev.to ([#11](https://github.com/JuanVqz/jekyll-devto/issues/11)) ([2e62edb](https://github.com/JuanVqz/jekyll-devto/commit/2e62edb47809cc945af3988973e6c44ad9fe2a27))
|
|
10
|
+
* **publisher:** use the post image as the dev.to cover ([#12](https://github.com/JuanVqz/jekyll-devto/issues/12)) ([90707e7](https://github.com/JuanVqz/jekyll-devto/commit/90707e71fbaa5d309bed29ddc455c04bea07237d))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Documentation
|
|
14
|
+
|
|
15
|
+
* add gem version and CI badges to the README ([#8](https://github.com/JuanVqz/jekyll-devto/issues/8)) ([a0d2a9c](https://github.com/JuanVqz/jekyll-devto/commit/a0d2a9c47a871597bd4d3f4c1ac113c18b41a323))
|
|
16
|
+
|
|
3
17
|
## 0.1.0 (2026-10-01)
|
|
4
18
|
|
|
5
19
|
|
data/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# jekyll-devto
|
|
2
2
|
|
|
3
|
+
[](https://badge.fury.io/rb/jekyll-devto)
|
|
4
|
+
[](https://github.com/JuanVqz/jekyll-devto/actions/workflows/ci.yml)
|
|
5
|
+
|
|
3
6
|
Cross-post a Jekyll blog to [dev.to](https://dev.to), complete, through dev.to's own RSS import.
|
|
4
7
|
|
|
5
8
|
dev.to can import posts from your feed, but with a typical Jekyll feed you get:
|
|
@@ -50,6 +53,19 @@ devto:
|
|
|
50
53
|
|
|
51
54
|
To keep a post off dev.to, set `devto: false` in its front matter.
|
|
52
55
|
|
|
56
|
+
**Cover image.** `jekyll-devto publish` gives each post a dev.to cover: `devto_cover` from the post's front matter, or else the post's `image` (a path, a URL, or `{ path: ... }`, which is what [jekyll-og-image](https://github.com/igor-alexandrov/jekyll-og-image) and Chirpy use). A cover already set on the dev.to draft is kept.
|
|
57
|
+
|
|
58
|
+
```yaml
|
|
59
|
+
devto_cover: /assets/img/devto-cover.png # optional
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Tags and series.** dev.to keeps only the first four tags, as lowercase letters and digits (`tailwind-css` becomes `tailwindcss`). `devto_tags` picks them for dev.to; it is used for the import and also replaces the tags of a draft that was already imported. `devto_series` puts the post in a dev.to series, created if it does not exist; a series already set on the draft is kept.
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
devto_tags: [ruby, rails, jekyll, opensource]
|
|
66
|
+
devto_series: "Rails upgrades"
|
|
67
|
+
```
|
|
68
|
+
|
|
53
69
|
## Publishing the drafts
|
|
54
70
|
|
|
55
71
|
```sh
|
|
@@ -89,9 +105,9 @@ Checked against [Forem's source](https://github.com/forem/forem), the software d
|
|
|
89
105
|
- **The imported body says `published: false` in its own front matter**, and that wins over the
|
|
90
106
|
API's `published` field (`Article#evaluate_front_matter`). The publish command flips it inside the
|
|
91
107
|
body. A plain `published: true` request leaves the post a draft.
|
|
92
|
-
- **
|
|
93
|
-
|
|
94
|
-
- **Only the first four tags are kept**, stripped to letters and digits.
|
|
108
|
+
- **The import drops the language of every code block.** dev.to removes every `class` attribute before converting (`Feeds::CleanHtml`), so a fence never gets a language from the feed. The feed marks each block with `data-lang`, which survives, and `jekyll-devto publish` writes that language into the draft's fences, matching blocks by their first line of code.
|
|
109
|
+
- **Sometimes the import keeps the raw HTML instead of Markdown.** dev.to converts the feed only when block tags outnumber blank lines (`Feeds::AssembleArticleMarkdown#html_content?`). The feed removes blank lines between tags and writes newlines inside code as ` `, so every post takes the Markdown path. Drafts imported before this change may still be raw HTML; their code keeps no language.
|
|
110
|
+
- **Only the first four tags are kept**, stripped to letters and digits and lowercased. Use `devto_tags` to choose them.
|
|
95
111
|
- **"Replace self-referential links with DEV-specific links"** rewrites links between your posts to
|
|
96
112
|
their dev.to articles at import time, drafts included. Publish the linked post first, or leave
|
|
97
113
|
that option off.
|
data/lib/jekyll/devto/feed.xml
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
{%- assign devto = site.devto | default: empty -%}
|
|
3
3
|
{%- comment -%} Not site.url + baseurl: links in content already carry baseurl (relative_url adds it). {%- endcomment -%}
|
|
4
4
|
{%- assign site_url = site.url -%}
|
|
5
|
+
{%- assign site_root = site.url | append: site.baseurl -%}
|
|
5
6
|
{%- assign posts = site.posts | where_exp: "post", "post.devto != false" -%}
|
|
6
7
|
{%- if devto.limit %}{% assign posts = posts | slice: 0, devto.limit %}{% endif %}
|
|
7
|
-
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
|
|
8
|
+
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:devto="https://github.com/JuanVqz/jekyll-devto">
|
|
8
9
|
<channel>
|
|
9
10
|
<title>{{ site.title | default: site.name | xml_escape }}</title>
|
|
10
11
|
<link>{{ "/" | absolute_url }}</link>
|
|
@@ -21,11 +22,25 @@
|
|
|
21
22
|
<link>{{ post_url }}</link>
|
|
22
23
|
<guid isPermaLink="true">{{ post_url }}</guid>
|
|
23
24
|
<pubDate>{{ post.date | date_to_rfc822 }}</pubDate>
|
|
24
|
-
{%-
|
|
25
|
+
{%- comment -%} devto_tags first, so new imports already carry the tags chosen for dev.to. {%- endcomment -%}
|
|
26
|
+
{%- assign devto_tags = post.devto_tags | devto_list %}
|
|
27
|
+
{%- assign tags = devto_tags | default: post.tags | default: post.categories %}
|
|
25
28
|
{%- for tag in tags %}
|
|
26
29
|
<category>{{ tag | xml_escape }}</category>
|
|
27
30
|
{%- endfor %}
|
|
28
31
|
<description>{{ post.description | default: post.excerpt | strip_html | strip_newlines | truncatewords: 60 | xml_escape }}</description>
|
|
32
|
+
{%- comment -%} The dev.to cover: devto_cover, else the post image (jekyll-og-image sets it). {%- endcomment -%}
|
|
33
|
+
{%- comment -%} Unlike content links, image paths leave baseurl out (Chirpy and jekyll-og-image add it when rendering). {%- endcomment -%}
|
|
34
|
+
{%- assign cover = post.devto_cover | default: post.image | devto_url: site_root %}
|
|
35
|
+
{%- if cover != "" %}
|
|
36
|
+
<devto:cover>{{ cover | xml_escape }}</devto:cover>
|
|
37
|
+
{%- endif %}
|
|
38
|
+
{%- if devto_tags %}
|
|
39
|
+
<devto:tags>{{ devto_tags | join: "," | xml_escape }}</devto:tags>
|
|
40
|
+
{%- endif %}
|
|
41
|
+
{%- if post.devto_series %}
|
|
42
|
+
<devto:series>{{ post.devto_series | xml_escape }}</devto:series>
|
|
43
|
+
{%- endif %}
|
|
29
44
|
<content:encoded>{{ post.content | devto_html: site_url | xml_escape }}</content:encoded>
|
|
30
45
|
</item>
|
|
31
46
|
{%- endfor %}
|
data/lib/jekyll/devto/html.rb
CHANGED
|
@@ -13,12 +13,16 @@ module Jekyll
|
|
|
13
13
|
# up inside the code.
|
|
14
14
|
# - Root-relative links and images point nowhere once the post lives on
|
|
15
15
|
# dev.to.
|
|
16
|
+
#
|
|
17
|
+
# The code language cannot travel as a class, so each block carries it as
|
|
18
|
+
# data-lang, which CleanHtml keeps. ReverseMarkdown ignores it; the
|
|
19
|
+
# publisher reads it from the feed and writes it into the draft's fences.
|
|
16
20
|
module HTML
|
|
17
21
|
# Kramdown block options such as {: .nolineno } or {: file="..." } add
|
|
18
22
|
# classes and attributes to the wrapper, so the class is matched anywhere
|
|
19
23
|
# in the tag.
|
|
20
24
|
ROUGE_BLOCK = %r{
|
|
21
|
-
<div\b[^>]*\bclass="[^"]*\bhighlighter-rouge\b[^"]*"[^>]
|
|
25
|
+
<div\b(?<attrs>[^>]*\bclass="[^"]*\bhighlighter-rouge\b[^"]*"[^>]*)>\s*
|
|
22
26
|
<div\ class="highlight">\s*<pre\ class="highlight"><code>
|
|
23
27
|
(?<body>.*?)
|
|
24
28
|
</code></pre>\s*</div>\s*</div>
|
|
@@ -26,7 +30,7 @@ module Jekyll
|
|
|
26
30
|
|
|
27
31
|
# What the {% highlight %} Liquid tag renders.
|
|
28
32
|
HIGHLIGHT_TAG = %r{
|
|
29
|
-
<figure\ class="highlight"><pre><code\b[^>]
|
|
33
|
+
<figure\ class="highlight"><pre><code\b(?<attrs>[^>]*)>
|
|
30
34
|
(?<body>.*?)
|
|
31
35
|
</code></pre></figure>
|
|
32
36
|
}mx
|
|
@@ -40,31 +44,103 @@ module Jekyll
|
|
|
40
44
|
TAG = /<[a-zA-Z][^>]*>/
|
|
41
45
|
ROOT_RELATIVE = %r{\b(src|href)="/(?!/)}
|
|
42
46
|
|
|
47
|
+
# Kramdown's name for a fence with no language.
|
|
48
|
+
NO_LANGUAGE = %w[plaintext text].freeze
|
|
49
|
+
|
|
43
50
|
module_function
|
|
44
51
|
|
|
45
52
|
def convert(html, base_url)
|
|
46
53
|
html.to_s
|
|
47
|
-
.gsub(ROUGE_BLOCK) { plain_code(Regexp.last_match[:body]) }
|
|
48
|
-
.gsub(HIGHLIGHT_TAG) { plain_code(Regexp.last_match[:body]) }
|
|
54
|
+
.gsub(ROUGE_BLOCK) { plain_code(Regexp.last_match[:body], rouge_language(Regexp.last_match[:attrs])) }
|
|
55
|
+
.gsub(HIGHLIGHT_TAG) { plain_code(Regexp.last_match[:body], Regexp.last_match[:attrs][/\bdata-lang="([^"]+)"/, 1]) }
|
|
49
56
|
.gsub(TAG) { |tag| tag.gsub(ROOT_RELATIVE, %(\\1="#{base_url.to_s.chomp('/')}/)) }
|
|
50
57
|
end
|
|
51
58
|
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
|
|
59
|
+
# Forem converts the HTML to Markdown only when block tags outnumber blank
|
|
60
|
+
# lines (Feeds::AssembleArticleMarkdown#html_content?); otherwise it stores
|
|
61
|
+
# the raw HTML, and none of the conversion (fences, languages) happens.
|
|
62
|
+
# Blank lines between tags are only formatting, and a newline inside code
|
|
63
|
+
# written as is the same character once parsed, so the post reads
|
|
64
|
+
# the same while the count of blank lines drops to zero.
|
|
65
|
+
def without_blank_lines(html)
|
|
66
|
+
html.split(%r{(<pre\b[^>]*>.*?</pre>)}m).map do |part|
|
|
67
|
+
part.start_with?('<pre') ? part.gsub("\n", ' ') : part.gsub(/\n\s*\n/, "\n")
|
|
68
|
+
end.join
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Kramdown writes the fence's language as a language-* class, and the
|
|
72
|
+
# language can hold any non-space character (c#, shell.session). It is
|
|
73
|
+
# read from the class attribute only, so another attribute such as
|
|
74
|
+
# file="docs/language-notes.md" cannot pass for it.
|
|
75
|
+
def rouge_language(attrs)
|
|
76
|
+
attrs[/\bclass="([^"]*)"/, 1].to_s[/(?:\A|\s)language-(\S+)/, 1]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def plain_code(body, language)
|
|
55
80
|
body = Regexp.last_match[:code] if body =~ GUTTER
|
|
81
|
+
attribute = language && !NO_LANGUAGE.include?(language) ? %( data-lang="#{language}") : ''
|
|
82
|
+
|
|
83
|
+
"<pre#{attribute}><code>#{body.gsub(/<[^>]+>/, '')}</code></pre>"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# A post image as an absolute URL. Takes a path, a URL, or Chirpy's
|
|
87
|
+
# { "path" => ... } hash; nil when there is nothing to point at.
|
|
88
|
+
def absolute_url(value, base_url)
|
|
89
|
+
path = value.is_a?(Hash) ? value['path'] : value
|
|
90
|
+
return if path.to_s.empty?
|
|
91
|
+
return path if path.match?(%r{\Ahttps?://})
|
|
92
|
+
return "#{base_url.to_s[/\A[a-z][a-z0-9+.-]*:/i] || 'https:'}#{path}" if path.start_with?('//')
|
|
93
|
+
|
|
94
|
+
"#{base_url.to_s.chomp('/')}/#{path.sub(%r{\A/}, '')}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
CODE_BLOCK = %r{<pre\b(?<attrs>[^>]*)><code\b[^>]*>(?<code>.*?)</code></pre>}m
|
|
98
|
+
|
|
99
|
+
# Every code block in a converted body, in order, as [language, first
|
|
100
|
+
# line of code] (language is nil when the block has none). The first line
|
|
101
|
+
# is what identifies the block in the dev.to draft, whose fences do not
|
|
102
|
+
# always line up one to one with the blocks.
|
|
103
|
+
def code_blocks(html)
|
|
104
|
+
html.to_s.scan(CODE_BLOCK).map do |attrs, code|
|
|
105
|
+
[attrs[/\bdata-lang="([^"]+)"/, 1], first_line(unescape(code))]
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Whitespace is collapsed for the comparison: some Markdown converters
|
|
110
|
+
# squeeze runs of spaces inside code.
|
|
111
|
+
def first_line(code)
|
|
112
|
+
code.to_s.each_line.map { |line| line.split.join(' ') }.find { |line| !line.empty? }.to_s
|
|
113
|
+
end
|
|
56
114
|
|
|
57
|
-
|
|
115
|
+
# The entities this HTML can carry inside code; enough to compare it with
|
|
116
|
+
# the Markdown dev.to stores. & goes last so "&lt;" stays "<".
|
|
117
|
+
def unescape(text)
|
|
118
|
+
text.gsub(/&#(\d+);/) { Regexp.last_match(1).to_i.chr(Encoding::UTF_8) }
|
|
119
|
+
.gsub('<', '<').gsub('>', '>').gsub('"', '"').gsub(''', "'").gsub('&', '&')
|
|
58
120
|
end
|
|
59
121
|
end
|
|
60
122
|
|
|
61
123
|
# Liquid filter for the feed template.
|
|
62
124
|
module Filters
|
|
125
|
+
# devto_tags as a list, whether the front matter wrote a YAML list or a
|
|
126
|
+
# string ("ruby, rails" or "ruby rails", as Jekyll accepts for tags), so
|
|
127
|
+
# the import and the publisher see the same tags. nil when there are none.
|
|
128
|
+
def devto_list(value)
|
|
129
|
+
list = value.is_a?(Array) ? value.map(&:to_s) : value.to_s.split(/[,\s]+/)
|
|
130
|
+
list = list.map(&:strip).reject(&:empty?)
|
|
131
|
+
list unless list.empty?
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def devto_url(value, base_url)
|
|
135
|
+
HTML.absolute_url(value, base_url).to_s
|
|
136
|
+
end
|
|
137
|
+
|
|
63
138
|
def devto_html(html, base_url)
|
|
64
|
-
HTML.convert(html, base_url)
|
|
139
|
+
HTML.without_blank_lines(HTML.convert(html, base_url))
|
|
65
140
|
end
|
|
66
141
|
end
|
|
67
142
|
end
|
|
68
143
|
end
|
|
69
144
|
|
|
70
|
-
|
|
145
|
+
# The CLI loads this file without Jekyll, only for HTML.code_blocks.
|
|
146
|
+
Liquid::Template.register_filter(Jekyll::Devto::Filters) if defined?(Liquid::Template)
|
|
@@ -4,6 +4,7 @@ require 'net/http'
|
|
|
4
4
|
require 'rexml/document'
|
|
5
5
|
require 'time'
|
|
6
6
|
require_relative 'client'
|
|
7
|
+
require_relative 'html'
|
|
7
8
|
|
|
8
9
|
module Jekyll
|
|
9
10
|
module Devto
|
|
@@ -17,7 +18,7 @@ module Jekyll
|
|
|
17
18
|
class Publisher
|
|
18
19
|
MAX_REDIRECTS = 5
|
|
19
20
|
|
|
20
|
-
Post = Struct.new(:title, :link, :date, keyword_init: true)
|
|
21
|
+
Post = Struct.new(:title, :link, :date, :code_blocks, :cover, :tags, :series, keyword_init: true)
|
|
21
22
|
|
|
22
23
|
def initialize(feed:, client:, days: 7, publish: false, now: Time.now, out: $stdout, err: $stderr)
|
|
23
24
|
@feed = feed
|
|
@@ -51,7 +52,7 @@ module Jekyll
|
|
|
51
52
|
|
|
52
53
|
# One rejected post must not stop the rest, or every retry would stop at it.
|
|
53
54
|
begin
|
|
54
|
-
result = @client.update(draft['id'], published: true, body_markdown: self.class.
|
|
55
|
+
result = @client.update(draft['id'], published: true, body_markdown: self.class.prepared_body(draft['body_markdown'], post, cover: new_cover(draft, post)))
|
|
55
56
|
sent[draft['id']] = post
|
|
56
57
|
@out.puts " sent #{post.title.inspect} -> #{result['url']}"
|
|
57
58
|
rescue StandardError => e
|
|
@@ -76,17 +77,102 @@ module Jekyll
|
|
|
76
77
|
end
|
|
77
78
|
end
|
|
78
79
|
|
|
80
|
+
# The draft body to send: published, with each code fence given the
|
|
81
|
+
# language its block had on the site.
|
|
82
|
+
def self.prepared_body(markdown, post, cover: post.cover)
|
|
83
|
+
body = published_body(markdown)
|
|
84
|
+
body = with_front_matter(body, 'cover_image', cover) if cover
|
|
85
|
+
tags = devto_tags(post.tags.to_a)
|
|
86
|
+
body = with_front_matter(body, 'tags', tags, replace: true) unless tags.empty?
|
|
87
|
+
body = with_front_matter(body, 'series', yaml_string(post.series)) if post.series
|
|
88
|
+
with_code_languages(body, post.code_blocks.to_a)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Adds `key: value` to the draft's front matter, which dev.to reads on
|
|
92
|
+
# every save (Article#evaluate_front_matter: cover_image, tags, series).
|
|
93
|
+
# A key the draft already has is kept, since it was set on dev.to on
|
|
94
|
+
# purpose, unless `replace:` says the site's value wins (devto_tags,
|
|
95
|
+
# which the author chose for dev.to explicitly).
|
|
96
|
+
def self.with_front_matter(markdown, key, value, replace: false)
|
|
97
|
+
# The key line plus any continuation lines (an indented or "- " list), so
|
|
98
|
+
# a replaced block list does not leave its items behind.
|
|
99
|
+
line = /^#{Regexp.escape(key)}:[^\r\n]*(?:\r?\n(?:[ \t]|-(?=[ \t\r\n]))[^\r\n]*)*/
|
|
100
|
+
markdown.to_s.sub(/\A---(\r?\n).*?^---[ \t]*\r?$/m) do |front_matter|
|
|
101
|
+
newline = Regexp.last_match(1)
|
|
102
|
+
if front_matter.match?(line)
|
|
103
|
+
next replace ? front_matter.sub(line) { "#{key}: #{value}" } : front_matter
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
front_matter.sub(/^---[ \t]*\r?\z/) { |closing| "#{key}: #{value}#{newline}#{closing}" }
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# The cleanup dev.to's import applies to tags (Feeds::AssembleArticleMarkdown
|
|
111
|
+
# #get_tags): at most four, spaces and anything but letters and digits
|
|
112
|
+
# removed, 20 characters each. Lowercased as dev.to stores them
|
|
113
|
+
# (ActsAsTaggableOn.force_lowercase), so the dry run shows the real tags.
|
|
114
|
+
def self.devto_tags(tags)
|
|
115
|
+
tags.first(4).map { |tag| tag.delete(' ').gsub(/[^[:alnum:]]/, '')[0..19].downcase }.reject(&:empty?).join(',')
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# A double-quoted YAML scalar, so a value such as "MDN: Spanish" stays a
|
|
119
|
+
# string instead of breaking the front matter. A JSON string is one.
|
|
120
|
+
def self.yaml_string(value)
|
|
121
|
+
JSON.generate(value.to_s)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Writes each block's language into the draft's code fence. Blocks are
|
|
125
|
+
# matched by their first line of code, not by position: dev.to does not
|
|
126
|
+
# turn every block into a fence (one inside a list item can come out
|
|
127
|
+
# unfenced), and an edited draft may not line up either. Each block is
|
|
128
|
+
# used once, in order: a fence only matches blocks after the last one
|
|
129
|
+
# matched, so a block missing from the draft cannot lend its language to
|
|
130
|
+
# a later fence that starts the same way. A fence nothing matches, or
|
|
131
|
+
# one that already names a language, is left alone.
|
|
132
|
+
def self.with_code_languages(markdown, blocks)
|
|
133
|
+
lines = markdown.to_s.lines
|
|
134
|
+
cursor = 0
|
|
135
|
+
inside = false
|
|
136
|
+
lines.each_with_index do |line, index|
|
|
137
|
+
next unless line =~ /\A\s*```[^\s`]*\s*\z/
|
|
138
|
+
|
|
139
|
+
inside = !inside
|
|
140
|
+
next unless inside && line =~ /\A\s*```\s*\z/
|
|
141
|
+
|
|
142
|
+
first = HTML.first_line(lines[(index + 1)..].take_while { |l| l !~ /\A\s*```/ }.join)
|
|
143
|
+
match = (cursor...blocks.size).find { |i| blocks[i][1] == first }
|
|
144
|
+
next unless match
|
|
145
|
+
|
|
146
|
+
cursor = match + 1
|
|
147
|
+
language = blocks[match].first
|
|
148
|
+
lines[index] = line.sub('```', "```#{language}") if language
|
|
149
|
+
end
|
|
150
|
+
lines.join
|
|
151
|
+
end
|
|
152
|
+
|
|
79
153
|
def due_posts
|
|
80
154
|
REXML::Document.new(read_feed).get_elements('//item').filter_map do |item|
|
|
81
155
|
date = Time.rfc2822(item.elements['pubDate'].text)
|
|
82
156
|
next if date > @now || date < @now - (@days * 86_400)
|
|
83
157
|
|
|
84
|
-
Post.new(title: item.elements['title'].text.to_s.strip, link: item.elements['link'].text.to_s.strip, date: date
|
|
158
|
+
Post.new(title: item.elements['title'].text.to_s.strip, link: item.elements['link'].text.to_s.strip, date: date,
|
|
159
|
+
code_blocks: HTML.code_blocks(item.elements['content:encoded']&.text),
|
|
160
|
+
cover: item.elements['devto:cover']&.text,
|
|
161
|
+
tags: item.elements['devto:tags']&.text&.split(','),
|
|
162
|
+
series: item.elements['devto:series']&.text)
|
|
85
163
|
end
|
|
86
164
|
end
|
|
87
165
|
|
|
88
166
|
private
|
|
89
167
|
|
|
168
|
+
# A cover chosen in dev.to's editor lives in the article (main_image),
|
|
169
|
+
# not in the body's front matter, and a cover_image key in the front
|
|
170
|
+
# matter would replace it (Article#set_main_image). The API reports it
|
|
171
|
+
# as cover_image, so a draft that has one is left with it.
|
|
172
|
+
def new_cover(draft, post)
|
|
173
|
+
post.cover if draft['cover_image'].to_s.empty?
|
|
174
|
+
end
|
|
175
|
+
|
|
90
176
|
# Follows redirects (http to https, apex to www), which Net::HTTP does not.
|
|
91
177
|
def read_feed
|
|
92
178
|
return File.read(@feed) unless @feed.match?(%r{\Ahttps?://})
|
data/lib/jekyll/devto/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jekyll-devto
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Juan Vásquez
|
|
@@ -48,7 +48,7 @@ description: |
|
|
|
48
48
|
Rouge line numbers, absolute links and images), and a command that
|
|
49
49
|
publishes the imported drafts once their post is live on your site.
|
|
50
50
|
email:
|
|
51
|
-
-
|
|
51
|
+
- hello@juanvasquez.dev
|
|
52
52
|
executables:
|
|
53
53
|
- jekyll-devto
|
|
54
54
|
extensions: []
|