ZMediumToMarkdown 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/ZMediumFetcher +274 -0
- data/lib/ErrorHandle.rb +7 -0
- data/lib/Helper.rb +25 -0
- data/lib/ImageDownloader.rb +19 -0
- data/lib/Models/Paragraph.rb +82 -0
- data/lib/Parsers/BQParser.rb +17 -0
- data/lib/Parsers/FallbackParser.rb +12 -0
- data/lib/Parsers/H1Parser.rb +17 -0
- data/lib/Parsers/H2Parser.rb +17 -0
- data/lib/Parsers/H3Parser.rb +17 -0
- data/lib/Parsers/H4Parser.rb +17 -0
- data/lib/Parsers/IMGParser.rb +33 -0
- data/lib/Parsers/IframeParser.rb +74 -0
- data/lib/Parsers/LinkParser.rb +47 -0
- data/lib/Parsers/MIXTAPEEMBEDParser.rb +21 -0
- data/lib/Parsers/MarkupParser.rb +26 -0
- data/lib/Parsers/OLIParser.rb +26 -0
- data/lib/Parsers/PParser.rb +22 -0
- data/lib/Parsers/PQParser.rb +17 -0
- data/lib/Parsers/PREParser.rb +17 -0
- data/lib/Parsers/Parser.rb +16 -0
- data/lib/Parsers/ULIParser.rb +26 -0
- data/lib/PathPolicy.rb +25 -0
- data/lib/Post.rb +43 -0
- data/lib/Request.rb +50 -0
- data/lib/User.rb +60 -0
- metadata +114 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4a64eb0d39c1be48b0ddd24863c802f4f76234ebeee73161cf4b74fe8e5e4392
|
4
|
+
data.tar.gz: 88f480de193e4d89069bc3e5fd0bd6efb83c25cac4d8b65b9ec4e96efd459102
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4a85b5bf8e8b98a52d859cdf1b144a2d7ede7dc5701d2695f36296a34f8f5e9004aa83092effb7ab2ed21d9ae9cf3ca3d669cb915c7e73d5f3342835c35cdc12
|
7
|
+
data.tar.gz: '019224020c3d4071f0a8f2ac86c1beb22abaca096862dbf8d50dbe14ca0fdbf37ff8d78dc9b88b42a5af40107595e07a8d83868d0409d57486bbf894ad8c2e63'
|
data/bin/ZMediumFetcher
ADDED
@@ -0,0 +1,274 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
|
4
|
+
$lib = File.expand_path('../lib', File.dirname(__FILE__))
|
5
|
+
$LOAD_PATH.unshift($lib)
|
6
|
+
|
7
|
+
require "open-uri"
|
8
|
+
require 'json'
|
9
|
+
require 'optparse'
|
10
|
+
|
11
|
+
require "Parsers/H1Parser"
|
12
|
+
require "Parsers/H2Parser"
|
13
|
+
require "Parsers/H3Parser"
|
14
|
+
require "Parsers/H4Parser"
|
15
|
+
require "Parsers/PParser"
|
16
|
+
require "Parsers/ULIParser"
|
17
|
+
require "Parsers/IframeParser"
|
18
|
+
require "Parsers/IMGParser"
|
19
|
+
require "Parsers/FallbackParser"
|
20
|
+
require "Parsers/BQParser"
|
21
|
+
require "Parsers/PREParser"
|
22
|
+
require "Parsers/MarkupParser"
|
23
|
+
require "Parsers/OLIParser"
|
24
|
+
require "Parsers/MIXTAPEEMBEDParser"
|
25
|
+
require "Parsers/PQParser"
|
26
|
+
require "Parsers/LinkParser"
|
27
|
+
|
28
|
+
require "PathPolicy"
|
29
|
+
require "Request"
|
30
|
+
require "Post"
|
31
|
+
require "User"
|
32
|
+
|
33
|
+
class Main
|
34
|
+
def initialize
|
35
|
+
fetcher = ZMediumFetcher.new
|
36
|
+
ARGV << '-h' if ARGV.empty?
|
37
|
+
OptionParser.new do |opts|
|
38
|
+
opts.banner = "Usage: ZMediumFetcher [options]"
|
39
|
+
|
40
|
+
opts.on('-uUSERNAME', '--username=USERNAME', 'test') do |username|
|
41
|
+
pathPolicy = PathPolicy.new("#{File.expand_path('../', File.dirname(__FILE__))}", "Output")
|
42
|
+
fetcher.downloadPostsByUsername(username, pathPolicy)
|
43
|
+
end
|
44
|
+
|
45
|
+
opts.on('-pPOST_URL', '--postURL=POST_URL', 'test') do |postURL|
|
46
|
+
pathPolicy = PathPolicy.new("#{File.expand_path('../', File.dirname(__FILE__))}", "Output")
|
47
|
+
fetcher.downloadPost(postURL, pathPolicy)
|
48
|
+
end
|
49
|
+
end.parse!
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class ZMediumFetcher
|
54
|
+
|
55
|
+
attr_accessor :progress, :linkParser
|
56
|
+
|
57
|
+
class Progress
|
58
|
+
attr_accessor :username, :postPath, :currentPostIndex, :totalPostsLength, :currentPostParagraphIndex, :totalPostParagraphsLength, :message
|
59
|
+
|
60
|
+
def printLog()
|
61
|
+
info = ""
|
62
|
+
if !username.nil?
|
63
|
+
if !currentPostIndex.nil? && !totalPostsLength.nil?
|
64
|
+
info += "[#{username}(#{currentPostIndex}/#{totalPostsLength})]"
|
65
|
+
else
|
66
|
+
info += "[#{username}]"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
if !postPath.nil?
|
71
|
+
if info != ""
|
72
|
+
info += "-"
|
73
|
+
end
|
74
|
+
if !currentPostParagraphIndex.nil? && !totalPostParagraphsLength.nil?
|
75
|
+
info += "[#{postPath[0..10]}...(#{currentPostParagraphIndex}/#{totalPostParagraphsLength})]"
|
76
|
+
else
|
77
|
+
info += "[#{postPath[0..10]}...]"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
if !message.nil?
|
82
|
+
if info != ""
|
83
|
+
info += "-"
|
84
|
+
end
|
85
|
+
info += message
|
86
|
+
end
|
87
|
+
|
88
|
+
if info != ""
|
89
|
+
puts info
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def initialize
|
95
|
+
@progress = Progress.new()
|
96
|
+
@linkParser = LinkParser.new(nil)
|
97
|
+
end
|
98
|
+
|
99
|
+
def buildParser(imagePathPolicy)
|
100
|
+
h1Parser = H1Parser.new()
|
101
|
+
h2Parser = H2Parser.new()
|
102
|
+
h1Parser.setNext(h2Parser)
|
103
|
+
h3Parser = H3Parser.new()
|
104
|
+
h2Parser.setNext(h3Parser)
|
105
|
+
h4Parser = H4Parser.new()
|
106
|
+
h3Parser.setNext(h4Parser)
|
107
|
+
ppParser = PParser.new()
|
108
|
+
h4Parser.setNext(ppParser)
|
109
|
+
uliParser = ULIParser.new()
|
110
|
+
ppParser.setNext(uliParser)
|
111
|
+
oliParser = OLIParser.new()
|
112
|
+
uliParser.setNext(oliParser)
|
113
|
+
mixtapeembedParser = MIXTAPEEMBEDParser.new()
|
114
|
+
oliParser.setNext(mixtapeembedParser)
|
115
|
+
pqParser = PQParser.new()
|
116
|
+
mixtapeembedParser.setNext(pqParser)
|
117
|
+
iframeParser = IframeParser.new()
|
118
|
+
iframeParser.pathPolicy = imagePathPolicy
|
119
|
+
pqParser.setNext(iframeParser)
|
120
|
+
imgParser = IMGParser.new()
|
121
|
+
imgParser.pathPolicy = imagePathPolicy
|
122
|
+
iframeParser.setNext(imgParser)
|
123
|
+
bqParser = BQParser.new()
|
124
|
+
imgParser.setNext(bqParser)
|
125
|
+
preParser = PREParser.new()
|
126
|
+
bqParser.setNext(preParser)
|
127
|
+
fallbackParser = FallbackParser.new()
|
128
|
+
preParser.setNext(fallbackParser)
|
129
|
+
|
130
|
+
|
131
|
+
h1Parser
|
132
|
+
end
|
133
|
+
|
134
|
+
def downloadPost(postURL, pathPolicy)
|
135
|
+
postID = Post.getPostIDFromPostURLString(postURL)
|
136
|
+
postPath = Post.getPostPathFromPostURLString(postURL)
|
137
|
+
|
138
|
+
progress.postPath = postPath
|
139
|
+
progress.message = "Downloading Post..."
|
140
|
+
progress.printLog()
|
141
|
+
|
142
|
+
postHtml = Request.html(Request.URL(postURL))
|
143
|
+
|
144
|
+
postContent = Post.parsePostContentFromHTML(postHtml)
|
145
|
+
if postContent.nil?
|
146
|
+
raise "Error: Content is empty! PostURL: #{postURL}"
|
147
|
+
end
|
148
|
+
|
149
|
+
sourceParagraphs = Post.parsePostParagraphsFromPostContent(postContent, postID)
|
150
|
+
if sourceParagraphs.nil?
|
151
|
+
raise "Error: Paragraph not found! PostURL: #{postURL}"
|
152
|
+
end
|
153
|
+
|
154
|
+
progress.message = "Formatting Data..."
|
155
|
+
progress.printLog()
|
156
|
+
|
157
|
+
paragraphs = []
|
158
|
+
oliIndex = 0
|
159
|
+
preParagraph = nil
|
160
|
+
sourceParagraphs.each do |sourcParagraph|
|
161
|
+
paragraph = Paragraph.new(sourcParagraph, postID, postContent)
|
162
|
+
if OLIParser.isOLI(paragraph)
|
163
|
+
oliIndex += 1
|
164
|
+
paragraph.oliIndex = oliIndex
|
165
|
+
else
|
166
|
+
oliIndex = 0
|
167
|
+
end
|
168
|
+
|
169
|
+
# if previous is OLI or ULI and current is not OLI or ULI
|
170
|
+
# than insert a blank paragraph to keep markdown foramt correct
|
171
|
+
if (OLIParser.isOLI(preParagraph) && !OLIParser.isOLI(paragraph)) ||
|
172
|
+
(ULIParser.isULI(preParagraph) && !ULIParser.isULI(paragraph))
|
173
|
+
paragraphs.append(Paragraph.makeBlankParagraph(postID))
|
174
|
+
end
|
175
|
+
|
176
|
+
paragraphs.append(paragraph)
|
177
|
+
preParagraph = paragraph
|
178
|
+
end
|
179
|
+
|
180
|
+
postPathPolicy = PathPolicy.new(pathPolicy.getAbsolutePath(nil), "posts")
|
181
|
+
|
182
|
+
imagePathPolicy = PathPolicy.new(postPathPolicy.getAbsolutePath(nil), "images")
|
183
|
+
startParser = buildParser(imagePathPolicy)
|
184
|
+
|
185
|
+
progress.totalPostParagraphsLength = paragraphs.length
|
186
|
+
progress.currentPostParagraphIndex = 0
|
187
|
+
progress.message = "Converting Post..."
|
188
|
+
progress.printLog()
|
189
|
+
|
190
|
+
absolutePath = postPathPolicy.getAbsolutePath("#{postPath}.md")
|
191
|
+
Helper.createDirIfNotExist(postPathPolicy.getAbsolutePath(nil))
|
192
|
+
index = 0
|
193
|
+
File.open(absolutePath, "w+") do |file|
|
194
|
+
paragraphs.each do |paragraph|
|
195
|
+
markupParser = MarkupParser.new(postHtml, paragraph)
|
196
|
+
paragraph.text = markupParser.parse()
|
197
|
+
result = startParser.parse(paragraph)
|
198
|
+
|
199
|
+
if !linkParser.nil?
|
200
|
+
result = linkParser.parse(result, paragraph.markupLinks)
|
201
|
+
end
|
202
|
+
|
203
|
+
file.puts(result)
|
204
|
+
|
205
|
+
index += 1
|
206
|
+
progress.currentPostParagraphIndex = index
|
207
|
+
progress.message = "Converting Post..."
|
208
|
+
progress.printLog()
|
209
|
+
end
|
210
|
+
|
211
|
+
file.puts(Helper.createWatermark(postURL))
|
212
|
+
end
|
213
|
+
|
214
|
+
progress.message = "Post Successfully Downloaded!"
|
215
|
+
progress.printLog()
|
216
|
+
|
217
|
+
progress.postPath = nil
|
218
|
+
end
|
219
|
+
|
220
|
+
def downloadPostsByUsername(username, pathPolicy)
|
221
|
+
progress.username = username
|
222
|
+
progress.message = "Fetching infromation..."
|
223
|
+
progress.printLog()
|
224
|
+
|
225
|
+
userID = User.convertToUserIDFromUsername(username)
|
226
|
+
if userID.nil?
|
227
|
+
raise "Medium's Username:#{username} not found!"
|
228
|
+
end
|
229
|
+
|
230
|
+
progress.message = "Fetching posts..."
|
231
|
+
progress.printLog()
|
232
|
+
|
233
|
+
postURLS = []
|
234
|
+
nextID = nil
|
235
|
+
begin
|
236
|
+
postPageInfo = User.fetchUserPosts(userID, nextID)
|
237
|
+
postPageInfo["postURLs"].each do |postURL|
|
238
|
+
postURLS.append(postURL)
|
239
|
+
end
|
240
|
+
nextID = postPageInfo["nextID"]
|
241
|
+
end while !nextID.nil?
|
242
|
+
|
243
|
+
@linkParser = LinkParser.new(postURLS)
|
244
|
+
|
245
|
+
progress.totalPostsLength = postURLS.length
|
246
|
+
progress.currentPostIndex = 0
|
247
|
+
progress.message = "Downloading posts..."
|
248
|
+
progress.printLog()
|
249
|
+
|
250
|
+
userPathPolicy = PathPolicy.new(pathPolicy.getAbsolutePath(nil), "users/#{username}")
|
251
|
+
index = 0
|
252
|
+
postURLS.each do |postURL|
|
253
|
+
downloadPost(postURL, userPathPolicy)
|
254
|
+
|
255
|
+
index += 1
|
256
|
+
progress.currentPostIndex = index
|
257
|
+
progress.message = "Downloading posts..."
|
258
|
+
progress.printLog()
|
259
|
+
end
|
260
|
+
|
261
|
+
progress.message = "All posts has been downloaded!, Total posts: #{postURLS.length}"
|
262
|
+
progress.printLog()
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
begin
|
267
|
+
puts "https://github.com/ZhgChgLi/ZMediumToMarkdown"
|
268
|
+
puts "You have read and agree with the Disclaimer."
|
269
|
+
Main.new()
|
270
|
+
puts "https://github.com/ZhgChgLi/ZMediumToMarkdown"
|
271
|
+
puts "If this repo is helpful, please help to star this repo or recommend it to your friends. Thanks."
|
272
|
+
rescue => e
|
273
|
+
puts "Error: #{e.class} #{e.message}"
|
274
|
+
end
|
data/lib/ErrorHandle.rb
ADDED
data/lib/Helper.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
$lib = File.expand_path('../lib', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class Helper
|
4
|
+
def self.createDirIfNotExist(dirPath)
|
5
|
+
dirs = dirPath.split("/")
|
6
|
+
currentDir = ""
|
7
|
+
begin
|
8
|
+
dir = dirs.shift
|
9
|
+
currentDir = "#{currentDir}/#{dir}"
|
10
|
+
Dir.mkdir(currentDir) unless File.exists?(currentDir)
|
11
|
+
end while dirs.length > 0
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.createWatermark(postURL)
|
15
|
+
text = "\r\n\r\n\r\n"
|
16
|
+
text += "+-----------------------------------------------------------------------------------+"
|
17
|
+
text += "\r\n"
|
18
|
+
text += "| **[View original post on Medium](#{postURL}) - Converted by [ZhgChgLi](https://blog.zhgchg.li)/[ZMediumToMarkdown](https://github.com/ZhgChgLi/ZMediumToMarkdown)** |"
|
19
|
+
text += "\r\n"
|
20
|
+
text += "+-----------------------------------------------------------------------------------+"
|
21
|
+
text += "\r\n"
|
22
|
+
|
23
|
+
text
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
$lib = File.expand_path('../lib', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require 'Helper'
|
4
|
+
|
5
|
+
class ImageDownloader
|
6
|
+
def self.download(path, url)
|
7
|
+
dir = path.split("/")
|
8
|
+
dir.pop()
|
9
|
+
Helper.createDirIfNotExist(dir.join("/"))
|
10
|
+
|
11
|
+
begin
|
12
|
+
imageResponse = open(url)
|
13
|
+
File.write(path, imageResponse.read, {mode: 'wb'})
|
14
|
+
true
|
15
|
+
rescue
|
16
|
+
false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
$lib = File.expand_path('../', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require 'Parsers/PParser'
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
class Paragraph
|
7
|
+
attr_accessor :postID, :name, :text, :type, :href, :metadata, :mixtapeMetadata, :iframe, :hasMarkup, :oliIndex, :markupLinks
|
8
|
+
|
9
|
+
class Iframe
|
10
|
+
attr_accessor :id, :title, :type, :src
|
11
|
+
def initialize(json)
|
12
|
+
@id = json['id']
|
13
|
+
@type = json['__typename']
|
14
|
+
@title = json['title']
|
15
|
+
@src = json['iframeSrc']
|
16
|
+
end
|
17
|
+
|
18
|
+
def parse()
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class MetaData
|
24
|
+
attr_accessor :id, :type
|
25
|
+
def initialize(json)
|
26
|
+
@id = json['id']
|
27
|
+
@type = json['__typename']
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class MixtapeMetadata
|
32
|
+
attr_accessor :href
|
33
|
+
def initialize(json)
|
34
|
+
@href = json['href']
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.makeBlankParagraph(postID)
|
39
|
+
json = {
|
40
|
+
"name" => "fakeBlankParagraph_#{SecureRandom.uuid}",
|
41
|
+
"text" => "",
|
42
|
+
"type" => PParser.getTypeString()
|
43
|
+
}
|
44
|
+
Paragraph.new(json, postID, nil)
|
45
|
+
end
|
46
|
+
|
47
|
+
def initialize(json, postID, resource)
|
48
|
+
@name = json['name']
|
49
|
+
@text = json['text']
|
50
|
+
@type = json['type']
|
51
|
+
@href = json['href']
|
52
|
+
@postID = postID
|
53
|
+
|
54
|
+
if json['metadata'].nil?
|
55
|
+
@metadata = nil
|
56
|
+
else
|
57
|
+
@metadata = MetaData.new(resource[json['metadata']['__ref']])
|
58
|
+
end
|
59
|
+
|
60
|
+
if json['mixtapeMetadata'].nil?
|
61
|
+
@mixtapeMetadata = nil
|
62
|
+
else
|
63
|
+
@mixtapeMetadata = MixtapeMetadata.new(json['mixtapeMetadata'])
|
64
|
+
end
|
65
|
+
|
66
|
+
if json['iframe'].nil?
|
67
|
+
@iframe = nil
|
68
|
+
else
|
69
|
+
@iframe = Iframe.new(resource[json['iframe']['mediaResource']['__ref']])
|
70
|
+
end
|
71
|
+
|
72
|
+
if !json['markups'].nil? && json['markups'].length > 0
|
73
|
+
links = json['markups'].select{ |markup| markup["type"] == "A" }
|
74
|
+
if !links.nil? && links.length > 0
|
75
|
+
@markupLinks = links.map{ |link| link["href"] }
|
76
|
+
end
|
77
|
+
@hasMarkup = true
|
78
|
+
else
|
79
|
+
@hasMarkup = false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
$lib = File.expand_path('../', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require "Parsers/Parser"
|
4
|
+
require 'Models/Paragraph'
|
5
|
+
|
6
|
+
class BQParser < Parser
|
7
|
+
attr_accessor :nextParser
|
8
|
+
def parse(paragraph)
|
9
|
+
if paragraph.type == 'BQ'
|
10
|
+
"> #{paragraph.text}"
|
11
|
+
else
|
12
|
+
if !nextParser.nil?
|
13
|
+
nextParser.parse(paragraph)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
$lib = File.expand_path('../', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require "Parsers/Parser"
|
4
|
+
require 'Models/Paragraph'
|
5
|
+
|
6
|
+
class H1Parser < Parser
|
7
|
+
attr_accessor :nextParser
|
8
|
+
def parse(paragraph)
|
9
|
+
if paragraph.type == 'H1'
|
10
|
+
"# #{paragraph.text}"
|
11
|
+
else
|
12
|
+
if !nextParser.nil?
|
13
|
+
nextParser.parse(paragraph)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
$lib = File.expand_path('../', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require "Parsers/Parser"
|
4
|
+
require 'Models/Paragraph'
|
5
|
+
|
6
|
+
class H2Parser < Parser
|
7
|
+
attr_accessor :nextParser
|
8
|
+
def parse(paragraph)
|
9
|
+
if paragraph.type == 'H2'
|
10
|
+
"## #{paragraph.text}"
|
11
|
+
else
|
12
|
+
if !nextParser.nil?
|
13
|
+
nextParser.parse(paragraph)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
$lib = File.expand_path('../', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require "Parsers/Parser"
|
4
|
+
require 'Models/Paragraph'
|
5
|
+
|
6
|
+
class H3Parser < Parser
|
7
|
+
attr_accessor :nextParser
|
8
|
+
def parse(paragraph)
|
9
|
+
if paragraph.type == 'H3'
|
10
|
+
"### #{paragraph.text}"
|
11
|
+
else
|
12
|
+
if !nextParser.nil?
|
13
|
+
nextParser.parse(paragraph)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
$lib = File.expand_path('../', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require "Parsers/Parser"
|
4
|
+
require 'Models/Paragraph'
|
5
|
+
|
6
|
+
class H4Parser < Parser
|
7
|
+
attr_accessor :nextParser
|
8
|
+
def parse(paragraph)
|
9
|
+
if paragraph.type == 'H4'
|
10
|
+
"#### #{paragraph.text}"
|
11
|
+
else
|
12
|
+
if !nextParser.nil?
|
13
|
+
nextParser.parse(paragraph)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
$lib = File.expand_path('../', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require "Parsers/Parser"
|
4
|
+
require 'Models/Paragraph'
|
5
|
+
|
6
|
+
require 'ImageDownloader'
|
7
|
+
require 'PathPolicy'
|
8
|
+
|
9
|
+
class IMGParser < Parser
|
10
|
+
attr_accessor :nextParser, :pathPolicy
|
11
|
+
def parse(paragraph)
|
12
|
+
if paragraph.type == 'IMG'
|
13
|
+
|
14
|
+
fileName = paragraph.metadata.id #d*fsafwfe.jpg
|
15
|
+
|
16
|
+
imageURL = "https://miro.medium.com/max/1400/#{paragraph.metadata.id}"
|
17
|
+
|
18
|
+
imagePathPolicy = PathPolicy.new(pathPolicy.getAbsolutePath(nil), paragraph.postID)
|
19
|
+
absolutePath = imagePathPolicy.getAbsolutePath(fileName)
|
20
|
+
|
21
|
+
if ImageDownloader.download(absolutePath, imageURL)
|
22
|
+
relativePath = "#{pathPolicy.getRelativePath(nil)}/#{imagePathPolicy.getRelativePath(fileName)}"
|
23
|
+
"![#{paragraph.text}](#{relativePath} \"#{paragraph.text}\")"
|
24
|
+
else
|
25
|
+
"![#{paragraph.text}](#{imageURL} \"#{paragraph.text}\")"
|
26
|
+
end
|
27
|
+
else
|
28
|
+
if !nextParser.nil?
|
29
|
+
nextParser.parse(paragraph)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
$lib = File.expand_path('../', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
require "Request"
|
6
|
+
require "Parsers/Parser"
|
7
|
+
require 'Models/Paragraph'
|
8
|
+
require 'nokogiri'
|
9
|
+
|
10
|
+
require 'ImageDownloader'
|
11
|
+
require 'PathPolicy'
|
12
|
+
|
13
|
+
class IframeParser < Parser
|
14
|
+
attr_accessor :nextParser, :pathPolicy
|
15
|
+
def parse(paragraph)
|
16
|
+
if paragraph.type == 'IFRAME'
|
17
|
+
if !paragraph.iframe.src.nil? && paragraph.iframe.src != ""
|
18
|
+
url = paragraph.iframe.src
|
19
|
+
else
|
20
|
+
url = "https://medium.com/media/#{paragraph.iframe.id}"
|
21
|
+
end
|
22
|
+
|
23
|
+
if !url[/(www\.youtube\.com)/].nil?
|
24
|
+
# is youtube
|
25
|
+
youtubeURL = URI(URI.decode(url)).query
|
26
|
+
params = URI::decode_www_form(youtubeURL).to_h
|
27
|
+
if !params["image"].nil? && !params["url"].nil?
|
28
|
+
|
29
|
+
fileName = "#{paragraph.name}_#{URI(params["image"]).path.split("/").last}" #21de_default.jpg
|
30
|
+
|
31
|
+
imageURL = params["image"]
|
32
|
+
imagePathPolicy = PathPolicy.new(pathPolicy.getAbsolutePath(nil), paragraph.postID)
|
33
|
+
absolutePath = imagePathPolicy.getAbsolutePath(fileName)
|
34
|
+
|
35
|
+
if ImageDownloader.download(absolutePath, imageURL)
|
36
|
+
relativePath = "#{pathPolicy.getRelativePath(nil)}/#{imagePathPolicy.getRelativePath(fileName)}"
|
37
|
+
result = "\n[![YouTube](#{relativePath} \"YouTube\")](#{params["url"]})"
|
38
|
+
else
|
39
|
+
result = "\n[YouTube](#{params["url"]})"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
else
|
43
|
+
html = Request.html(Request.URL(url))
|
44
|
+
src = html.search('script').first.attribute('src')
|
45
|
+
result = nil
|
46
|
+
if !src.to_s[/^(https\:\/\/gist\.github\.com)/].nil?
|
47
|
+
# is gist
|
48
|
+
gist = Request.body(Request.URL(src)).scan(/(document\.write\('){1}(.*)(\)){1}/)[1][1]
|
49
|
+
gist.gsub! '\n', ''
|
50
|
+
gist.gsub! '\"', '"'
|
51
|
+
gist.gsub! '<\/', '</'
|
52
|
+
gistHTML = Nokogiri::HTML(gist)
|
53
|
+
lang = gistHTML.search('table').first['data-tagsearch-lang']
|
54
|
+
gistHTML.search('a').each do |a|
|
55
|
+
if a.text == 'view raw'
|
56
|
+
gistRAW = Request.body(Request.URL(a['href']))
|
57
|
+
result = "```#{lang}\n#{gistRAW}\n```"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
if result.nil?
|
64
|
+
"[#{paragraph.iframe.title}](#{url})"
|
65
|
+
else
|
66
|
+
result
|
67
|
+
end
|
68
|
+
else
|
69
|
+
if !nextParser.nil?
|
70
|
+
nextParser.parse(paragraph)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
$lib = File.expand_path('../', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require 'Models/Paragraph'
|
4
|
+
|
5
|
+
class LinkParser
|
6
|
+
attr_accessor :usersPostURLs
|
7
|
+
|
8
|
+
def initialize(usersPostURLs)
|
9
|
+
@usersPostURLs = usersPostURLs
|
10
|
+
end
|
11
|
+
|
12
|
+
def parse(markdownString, markupLinks)
|
13
|
+
if !markupLinks.nil?
|
14
|
+
matchLinks = markdownString.scan(/\[[^\]]*\]\(([^\)]*)\)/)
|
15
|
+
if !matchLinks.nil?
|
16
|
+
|
17
|
+
matchLinks.each do |matchLink|
|
18
|
+
link = matchLink[0]
|
19
|
+
|
20
|
+
if !usersPostURLs.nil?
|
21
|
+
# if have provide user's post urls
|
22
|
+
# find & replace medium url to local post url if matched
|
23
|
+
|
24
|
+
postPath = link.split("/").last
|
25
|
+
if !usersPostURLs.find { |usersPostURL| usersPostURL.split("/").last.split("-").last == postPath.split("-").last }.nil?
|
26
|
+
markdownString = markdownString.sub! link, postPath
|
27
|
+
end
|
28
|
+
else
|
29
|
+
if !(link =~ /\A#{URI::regexp(['http', 'https'])}\z/)
|
30
|
+
# medium will give you an relative path if url is medium's post (due to we use html to markdown render)
|
31
|
+
# e.g. /zrealm-ios-dev/visitor-pattern-in-ios-swift-ba5773a7bfea
|
32
|
+
# it's not a vaild url
|
33
|
+
|
34
|
+
# fullfill url from markup attribute
|
35
|
+
match = markupLinks.find{ |markupLink| markupLink.include? link }
|
36
|
+
if !match.nil?
|
37
|
+
markdownString = markdownString.sub! link, match
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
markdownString
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
$lib = File.expand_path('../', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require "Parsers/Parser"
|
4
|
+
require 'Models/Paragraph'
|
5
|
+
|
6
|
+
class MIXTAPEEMBEDParser < Parser
|
7
|
+
attr_accessor :nextParser
|
8
|
+
def parse(paragraph)
|
9
|
+
if paragraph.type == 'MIXTAPE_EMBED'
|
10
|
+
if !paragraph.mixtapeMetadata.nil? && !paragraph.mixtapeMetadata.href.nil?
|
11
|
+
"\n[#{paragraph.text}](#{paragraph.mixtapeMetadata.href})"
|
12
|
+
else
|
13
|
+
"\n#{paragraph.text}"
|
14
|
+
end
|
15
|
+
else
|
16
|
+
if !nextParser.nil?
|
17
|
+
nextParser.parse(paragraph)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
$lib = File.expand_path('../', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require 'Models/Paragraph'
|
4
|
+
require 'reverse_markdown'
|
5
|
+
require 'nokogiri'
|
6
|
+
|
7
|
+
class MarkupParser
|
8
|
+
attr_accessor :body, :paragraph
|
9
|
+
|
10
|
+
def initialize(html, paragraph)
|
11
|
+
@body = html.search("body").first
|
12
|
+
@paragraph = paragraph
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse()
|
16
|
+
result = paragraph.text
|
17
|
+
if paragraph.hasMarkup
|
18
|
+
p = body.at_css("##{paragraph.name}")
|
19
|
+
if !p.nil?
|
20
|
+
result = ReverseMarkdown.convert p.inner_html
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
result
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
$lib = File.expand_path('../', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require "Parsers/Parser"
|
4
|
+
require 'Models/Paragraph'
|
5
|
+
|
6
|
+
class OLIParser < Parser
|
7
|
+
attr_accessor :nextParser, :oliIndex
|
8
|
+
|
9
|
+
def self.isOLI(paragraph)
|
10
|
+
if paragraph.nil?
|
11
|
+
false
|
12
|
+
else
|
13
|
+
paragraph.type == "OLI"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse(paragraph)
|
18
|
+
if OLIParser.isOLI(paragraph)
|
19
|
+
"#{oliIndex}. #{paragraph.text}"
|
20
|
+
else
|
21
|
+
if !nextParser.nil?
|
22
|
+
nextParser.parse(paragraph)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
$lib = File.expand_path('../', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require "Parsers/Parser"
|
4
|
+
require 'Models/Paragraph'
|
5
|
+
|
6
|
+
class PParser < Parser
|
7
|
+
attr_accessor :nextParser
|
8
|
+
|
9
|
+
def self.getTypeString()
|
10
|
+
'P'
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse(paragraph)
|
14
|
+
if paragraph.type == PParser.getTypeString()
|
15
|
+
"\n#{paragraph.text}"
|
16
|
+
else
|
17
|
+
if !nextParser.nil?
|
18
|
+
nextParser.parse(paragraph)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
$lib = File.expand_path('../', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require "Parsers/Parser"
|
4
|
+
require 'Models/Paragraph'
|
5
|
+
|
6
|
+
class PQParser < Parser
|
7
|
+
attr_accessor :nextParser
|
8
|
+
def parse(paragraph)
|
9
|
+
if paragraph.type == 'PQ'
|
10
|
+
"> #{paragraph.text}"
|
11
|
+
else
|
12
|
+
if !nextParser.nil?
|
13
|
+
nextParser.parse(paragraph)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
$lib = File.expand_path('../', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require "Parsers/Parser"
|
4
|
+
require 'Models/Paragraph'
|
5
|
+
|
6
|
+
class PREParser < Parser
|
7
|
+
attr_accessor :nextParser
|
8
|
+
def parse(paragraph)
|
9
|
+
if paragraph.type == 'PRE'
|
10
|
+
"> #{paragraph.text}"
|
11
|
+
else
|
12
|
+
if !nextParser.nil?
|
13
|
+
nextParser.parse(paragraph)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
$lib = File.expand_path('../', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require "Parsers/Parser"
|
4
|
+
require 'Models/Paragraph'
|
5
|
+
|
6
|
+
class ULIParser < Parser
|
7
|
+
attr_accessor :nextParser
|
8
|
+
|
9
|
+
def self.isULI(paragraph)
|
10
|
+
if paragraph.nil?
|
11
|
+
false
|
12
|
+
else
|
13
|
+
paragraph.type == "ULI"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse(paragraph)
|
18
|
+
if ULIParser.isULI(paragraph)
|
19
|
+
"- #{paragraph.text}"
|
20
|
+
else
|
21
|
+
if !nextParser.nil?
|
22
|
+
nextParser.parse(paragraph)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/PathPolicy.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
$lib = File.expand_path('../lib', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class PathPolicy
|
4
|
+
attr_accessor :rootPath, :path
|
5
|
+
def initialize(rootPath, path)
|
6
|
+
@rootPath = rootPath
|
7
|
+
@path = path
|
8
|
+
end
|
9
|
+
|
10
|
+
def getRelativePath(lastPath)
|
11
|
+
if lastPath.nil?
|
12
|
+
"#{path}"
|
13
|
+
else
|
14
|
+
"#{path}/#{lastPath}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def getAbsolutePath(lastPath)
|
19
|
+
if lastPath.nil?
|
20
|
+
"#{rootPath}/#{path}"
|
21
|
+
else
|
22
|
+
"#{rootPath}/#{path}/#{lastPath}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/Post.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
$lib = File.expand_path('../lib', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require "Request"
|
4
|
+
require 'uri'
|
5
|
+
require 'nokogiri'
|
6
|
+
require 'json'
|
7
|
+
|
8
|
+
class Post
|
9
|
+
def self.getPostIDFromPostURLString(postURLString)
|
10
|
+
uri = URI.parse(postURLString)
|
11
|
+
postID = uri.path.split('/').last.split('-').last
|
12
|
+
|
13
|
+
postID
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.getPostPathFromPostURLString(postURLString)
|
17
|
+
uri = URI.parse(postURLString)
|
18
|
+
postPath = uri.path.split('/').last
|
19
|
+
|
20
|
+
URI.decode(postPath)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.parsePostContentFromHTML(html)
|
24
|
+
json = nil
|
25
|
+
html.search('script').each do |script|
|
26
|
+
match = script.to_s[/(<script>window\.__APOLLO_STATE__ \= ){1}(.*)(<\/script>){1}/,2]
|
27
|
+
if !match.nil? && match != ""
|
28
|
+
json = JSON.parse(match)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
json
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.parsePostParagraphsFromPostContent(content, postID)
|
36
|
+
result = content&.dig("Post:#{postID}", "content({\"postMeteringOptions\":null})", "bodyModel", "paragraphs")
|
37
|
+
if result.nil?
|
38
|
+
nil
|
39
|
+
else
|
40
|
+
result.map { |paragraph| content[paragraph["__ref"]] }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/Request.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
$lib = File.expand_path('../lib', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'nokogiri'
|
5
|
+
|
6
|
+
class Request
|
7
|
+
def self.URL(url, method = 'GET', data = nil, retryCount = 0)
|
8
|
+
retryCount += 1
|
9
|
+
|
10
|
+
uri = URI(url)
|
11
|
+
https = Net::HTTP.new(uri.host, uri.port)
|
12
|
+
https.use_ssl = true
|
13
|
+
|
14
|
+
if method.upcase == "GET"
|
15
|
+
request = Net::HTTP::Get.new(uri)
|
16
|
+
request['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.17.375.766 Safari/537.36';
|
17
|
+
else
|
18
|
+
request = Net::HTTP::Post.new(uri)
|
19
|
+
request['Content-Type'] = 'application/json'
|
20
|
+
if !data.nil?
|
21
|
+
request.body = JSON.dump(data)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
response = https.request(request)
|
26
|
+
# 3XX Redirect
|
27
|
+
if response.code.to_i >= 300 && response.code.to_i <= 399 && !response['location'].nil? && response['location'] != ''
|
28
|
+
if retryCount >= 10
|
29
|
+
raise "Error: Retry limit reached. path: #{url}"
|
30
|
+
else
|
31
|
+
response = self.URL(response['location'], method, data)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
response
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.html(response)
|
38
|
+
if response.code.to_i != 200
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
Nokogiri::HTML(response.read_body)
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.body(response)
|
45
|
+
if response.code.to_i != 200
|
46
|
+
nil
|
47
|
+
end
|
48
|
+
response.read_body
|
49
|
+
end
|
50
|
+
end
|
data/lib/User.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
$lib = File.expand_path('../lib', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require "Request"
|
4
|
+
require 'uri'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
class User
|
8
|
+
def self.convertToUserIDFromUsername(username)
|
9
|
+
if username.match(/^@/)
|
10
|
+
username = username[1...]
|
11
|
+
end
|
12
|
+
|
13
|
+
html = Request.html(Request.URL("https://medium.com/@#{username}"))
|
14
|
+
userId = nil
|
15
|
+
html.search('script').each do |script|
|
16
|
+
match = script.to_s[/(userId\:){1}(([0-9|a-z|A-Z])+)/,2]
|
17
|
+
if !match.nil? && match != ""
|
18
|
+
userId = match
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
userId
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.fetchUserPosts(userID, from)
|
26
|
+
query = [
|
27
|
+
{
|
28
|
+
"operationName": "UserProfileQuery",
|
29
|
+
"variables": {
|
30
|
+
"homepagePostsFrom": from,
|
31
|
+
"includeDistributedResponses": true,
|
32
|
+
"id": userID,
|
33
|
+
"homepagePostsLimit": 10
|
34
|
+
},
|
35
|
+
"query": "query UserProfileQuery($id: ID, $username: ID, $homepagePostsLimit: PaginationLimit, $homepagePostsFrom: String = null, $includeDistributedResponses: Boolean = true) {\n userResult(id: $id, username: $username) {\n __typename\n ... on User {\n id\n name\n viewerIsUser\n viewerEdge {\n id\n isFollowing\n __typename\n }\n homePostsPublished: homepagePostsConnection(paging: {limit: 1}) {\n posts {\n id\n __typename\n }\n __typename\n }\n ...UserCanonicalizer_user\n ...UserProfileScreen_user\n ...EntityDrivenSubscriptionLandingPageScreen_writer\n ...useShouldShowEntityDrivenSubscription_creator\n __typename\n }\n }\n}\n\nfragment UserCanonicalizer_user on User {\n id\n username\n hasSubdomain\n customDomainState {\n live {\n domain\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment UserProfileScreen_user on User {\n __typename\n id\n viewerIsUser\n ...PublisherHeader_publisher\n ...PublisherHomePosts_publisher\n ...UserSubdomainFlow_user\n ...UserProfileMetadata_user\n ...SuspendedBannerLoader_user\n ...ExpandablePost_user\n ...useAnalytics_user\n}\n\nfragment PublisherHeader_publisher on Publisher {\n id\n ...PublisherHeaderBackground_publisher\n ...PublisherHeaderNameplate_publisher\n ...PublisherHeaderActions_publisher\n ...PublisherHeaderNav_publisher\n __typename\n}\n\nfragment PublisherHeaderBackground_publisher on Publisher {\n __typename\n id\n customStyleSheet {\n ...PublisherHeaderBackground_customStyleSheet\n __typename\n id\n }\n ... on Collection {\n colorPalette {\n tintBackgroundSpectrum {\n backgroundColor\n __typename\n }\n __typename\n }\n isAuroraVisible\n legacyHeaderBackgroundImage {\n id\n originalWidth\n focusPercentX\n focusPercentY\n __typename\n }\n ...collectionTintBackgroundTheme_collection\n __typename\n id\n }\n ...publisherUrl_publisher\n}\n\nfragment PublisherHeaderBackground_customStyleSheet on CustomStyleSheet {\n id\n global {\n colorPalette {\n background {\n rgb\n __typename\n }\n __typename\n }\n __typename\n }\n header {\n headerScale\n backgroundImageDisplayMode\n backgroundImageVerticalAlignment\n backgroundColorDisplayMode\n backgroundColor {\n alpha\n rgb\n ...getHexFromColorValue_colorValue\n ...getOpaqueHexFromColorValue_colorValue\n __typename\n }\n secondaryBackgroundColor {\n ...getHexFromColorValue_colorValue\n __typename\n }\n postBackgroundColor {\n ...getHexFromColorValue_colorValue\n __typename\n }\n backgroundImage {\n ...MetaHeaderBackground_imageMetadata\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment getHexFromColorValue_colorValue on ColorValue {\n rgb\n alpha\n __typename\n}\n\nfragment getOpaqueHexFromColorValue_colorValue on ColorValue {\n rgb\n __typename\n}\n\nfragment MetaHeaderBackground_imageMetadata on ImageMetadata {\n id\n originalWidth\n __typename\n}\n\nfragment collectionTintBackgroundTheme_collection on Collection {\n colorPalette {\n ...collectionTintBackgroundTheme_colorPalette\n __typename\n }\n customStyleSheet {\n id\n ...collectionTintBackgroundTheme_customStyleSheet\n __typename\n }\n __typename\n id\n}\n\nfragment collectionTintBackgroundTheme_colorPalette on ColorPalette {\n ...customTintBackgroundTheme_colorPalette\n __typename\n}\n\nfragment customTintBackgroundTheme_colorPalette on ColorPalette {\n tintBackgroundSpectrum {\n ...ThemeUtil_colorSpectrum\n __typename\n }\n __typename\n}\n\nfragment ThemeUtil_colorSpectrum on ColorSpectrum {\n backgroundColor\n ...ThemeUtilInterpolateHelpers_colorSpectrum\n __typename\n}\n\nfragment ThemeUtilInterpolateHelpers_colorSpectrum on ColorSpectrum {\n colorPoints {\n ...ThemeUtil_colorPoint\n __typename\n }\n __typename\n}\n\nfragment ThemeUtil_colorPoint on ColorPoint {\n color\n point\n __typename\n}\n\nfragment collectionTintBackgroundTheme_customStyleSheet on CustomStyleSheet {\n id\n ...customTintBackgroundTheme_customStyleSheet\n __typename\n}\n\nfragment customTintBackgroundTheme_customStyleSheet on CustomStyleSheet {\n id\n global {\n colorPalette {\n primary {\n colorPalette {\n ...customTintBackgroundTheme_colorPalette\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment publisherUrl_publisher on Publisher {\n id\n __typename\n ... on Collection {\n ...collectionUrl_collection\n __typename\n id\n }\n ... on User {\n ...userUrl_user\n __typename\n id\n }\n}\n\nfragment collectionUrl_collection on Collection {\n id\n domain\n slug\n __typename\n}\n\nfragment userUrl_user on User {\n __typename\n id\n customDomainState {\n live {\n domain\n __typename\n }\n __typename\n }\n hasSubdomain\n username\n}\n\nfragment PublisherHeaderNameplate_publisher on Publisher {\n ...PublisherAvatar_publisher\n ...PublisherHeaderLogo_publisher\n ...PublisherFollowerCount_publisher\n __typename\n}\n\nfragment PublisherAvatar_publisher on Publisher {\n __typename\n ... on Collection {\n id\n ...CollectionAvatar_collection\n __typename\n }\n ... on User {\n id\n ...UserAvatar_user\n __typename\n }\n}\n\nfragment CollectionAvatar_collection on Collection {\n name\n avatar {\n id\n __typename\n }\n ...collectionUrl_collection\n __typename\n id\n}\n\nfragment UserAvatar_user on User {\n __typename\n id\n imageId\n mediumMemberAt\n name\n username\n ...userUrl_user\n}\n\nfragment PublisherHeaderLogo_publisher on Publisher {\n __typename\n id\n customStyleSheet {\n id\n header {\n logoImage {\n id\n originalHeight\n originalWidth\n __typename\n }\n appNameColor {\n ...getHexFromColorValue_colorValue\n __typename\n }\n appNameTreatment\n __typename\n }\n __typename\n }\n name\n ... on Collection {\n isAuroraVisible\n logo {\n id\n originalHeight\n originalWidth\n __typename\n }\n __typename\n id\n }\n ...CustomHeaderTooltip_publisher\n ...publisherUrl_publisher\n}\n\nfragment CustomHeaderTooltip_publisher on Publisher {\n __typename\n id\n customStyleSheet {\n id\n header {\n appNameTreatment\n nameTreatment\n __typename\n }\n __typename\n }\n ... on Collection {\n isAuroraVisible\n slug\n __typename\n id\n }\n}\n\nfragment PublisherFollowerCount_publisher on Publisher {\n __typename\n id\n ... on Collection {\n slug\n subscriberCount\n __typename\n id\n }\n ... on User {\n socialStats {\n followerCount\n __typename\n }\n username\n __typename\n id\n }\n}\n\nfragment PublisherHeaderActions_publisher on Publisher {\n __typename\n ...MetaHeaderPubMenu_publisher\n ... on Collection {\n ...CollectionFollowButton_collection\n __typename\n id\n }\n ... on User {\n ...FollowAndSubscribeButtons_user\n __typename\n id\n }\n}\n\nfragment MetaHeaderPubMenu_publisher on Publisher {\n __typename\n ... on Collection {\n ...MetaHeaderPubMenu_publisher_collection\n __typename\n id\n }\n ... on User {\n ...MetaHeaderPubMenu_publisher_user\n __typename\n id\n }\n}\n\nfragment MetaHeaderPubMenu_publisher_collection on Collection {\n id\n slug\n name\n domain\n newsletterV3 {\n slug\n __typename\n id\n }\n ...MutePopoverOptions_collection\n __typename\n}\n\nfragment MutePopoverOptions_collection on Collection {\n id\n __typename\n}\n\nfragment MetaHeaderPubMenu_publisher_user on User {\n id\n username\n ...MutePopoverOptions_creator\n __typename\n}\n\nfragment MutePopoverOptions_creator on User {\n id\n __typename\n}\n\nfragment CollectionFollowButton_collection on Collection {\n __typename\n id\n name\n slug\n ...collectionUrl_collection\n ...SusiClickable_collection\n}\n\nfragment SusiClickable_collection on Collection {\n ...SusiContainer_collection\n __typename\n id\n}\n\nfragment SusiContainer_collection on Collection {\n name\n ...SignInOptions_collection\n ...SignUpOptions_collection\n __typename\n id\n}\n\nfragment SignInOptions_collection on Collection {\n id\n name\n __typename\n}\n\nfragment SignUpOptions_collection on Collection {\n id\n name\n __typename\n}\n\nfragment FollowAndSubscribeButtons_user on User {\n ...UserFollowButton_user\n ...UserSubscribeButton_user\n __typename\n id\n}\n\nfragment UserFollowButton_user on User {\n ...UserFollowButtonSignedIn_user\n ...UserFollowButtonSignedOut_user\n __typename\n id\n}\n\nfragment UserFollowButtonSignedIn_user on User {\n id\n __typename\n}\n\nfragment UserFollowButtonSignedOut_user on User {\n id\n ...SusiClickable_user\n __typename\n}\n\nfragment SusiClickable_user on User {\n ...SusiContainer_user\n __typename\n id\n}\n\nfragment SusiContainer_user on User {\n ...SignInOptions_user\n ...SignUpOptions_user\n __typename\n id\n}\n\nfragment SignInOptions_user on User {\n id\n name\n __typename\n}\n\nfragment SignUpOptions_user on User {\n id\n name\n __typename\n}\n\nfragment UserSubscribeButton_user on User {\n id\n isPartnerProgramEnrolled\n name\n viewerEdge {\n id\n isFollowing\n isUser\n __typename\n }\n viewerIsUser\n newsletterV3 {\n id\n ...useNewsletterV3Subscription_newsletterV3\n __typename\n }\n ...useNewsletterV3Subscription_user\n ...MembershipUpsellModal_user\n __typename\n}\n\nfragment useNewsletterV3Subscription_newsletterV3 on NewsletterV3 {\n id\n type\n slug\n name\n collection {\n slug\n __typename\n id\n }\n user {\n id\n name\n username\n newsletterV3 {\n id\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment useNewsletterV3Subscription_user on User {\n id\n username\n newsletterV3 {\n ...useNewsletterV3Subscription_newsletterV3\n __typename\n id\n }\n __typename\n}\n\nfragment MembershipUpsellModal_user on User {\n id\n name\n imageId\n postSubscribeMembershipUpsellShownAt\n newsletterV3 {\n id\n __typename\n }\n __typename\n}\n\nfragment PublisherHeaderNav_publisher on Publisher {\n __typename\n id\n customStyleSheet {\n navigation {\n navItems {\n name\n ...PublisherHeaderNavLink_headerNavigationItem\n __typename\n }\n __typename\n }\n __typename\n id\n }\n ...PublisherHeaderNavLink_publisher\n ... on Collection {\n domain\n isAuroraVisible\n slug\n navItems {\n tagSlug\n title\n url\n __typename\n }\n __typename\n id\n }\n ... on User {\n customDomainState {\n live {\n domain\n __typename\n }\n __typename\n }\n hasSubdomain\n username\n about\n homePostsPublished: homepagePostsConnection(paging: {limit: 1}) {\n posts {\n id\n __typename\n }\n __typename\n }\n __typename\n id\n }\n}\n\nfragment PublisherHeaderNavLink_headerNavigationItem on HeaderNavigationItem {\n href\n name\n tags {\n id\n normalizedTagSlug\n __typename\n }\n type\n __typename\n}\n\nfragment PublisherHeaderNavLink_publisher on Publisher {\n __typename\n id\n ... on Collection {\n slug\n __typename\n id\n }\n}\n\nfragment PublisherHomePosts_publisher on Publisher {\n __typename\n id\n homepagePostsConnection(\n paging: {limit: $homepagePostsLimit, from: $homepagePostsFrom}\n includeDistributedResponses: $includeDistributedResponses\n ) {\n posts {\n ...PublisherHomePosts_post\n __typename\n }\n pagingInfo {\n next {\n from\n limit\n __typename\n }\n __typename\n }\n __typename\n }\n ...CardByline_publisher\n ...NewsletterV3Promo_publisher\n ...PublisherHomePosts_user\n}\n\nfragment PublisherHomePosts_post on Post {\n id\n collection {\n id\n name\n ...collectionUrl_collection\n __typename\n }\n ...ExpandablePost_post\n __typename\n}\n\nfragment ExpandablePost_post on Post {\n id\n creator {\n ...ExpandablePost_user\n __typename\n id\n }\n collection {\n ...CardByline_collection\n __typename\n id\n }\n ...InteractivePostBody_postPreview\n firstPublishedAt\n isLocked\n isSeries\n isShortform\n latestPublishedAt\n inResponseToCatalogResult {\n __typename\n }\n mediumUrl\n postResponses {\n count\n __typename\n }\n previewImage {\n id\n focusPercentX\n focusPercentY\n __typename\n }\n readingTime\n sequence {\n slug\n __typename\n }\n title\n uniqueSlug\n visibility\n ...CardByline_post\n ...ExpandablePostFooter_post\n ...InResponseToEntityPreview_post\n ...PostScrollTracker_post\n ...ReadMore_post\n ...HighDensityPreview_post\n __typename\n}\n\nfragment ExpandablePost_user on User {\n __typename\n name\n username\n ...CardByline_user\n id\n}\n\nfragment CardByline_user on User {\n __typename\n id\n name\n username\n mediumMemberAt\n socialStats {\n followerCount\n __typename\n }\n ...userUrl_user\n ...UserMentionTooltip_user\n}\n\nfragment UserMentionTooltip_user on User {\n id\n name\n username\n bio\n imageId\n mediumMemberAt\n ...UserAvatar_user\n ...UserFollowButton_user\n __typename\n}\n\nfragment CardByline_collection on Collection {\n __typename\n id\n name\n ...collectionUrl_collection\n}\n\nfragment InteractivePostBody_postPreview on Post {\n extendedPreviewContent(\n truncationConfig: {previewParagraphsWordCountThreshold: 400, minimumWordLengthForTruncation: 150, truncateAtEndOfSentence: true, showFullImageCaptions: true, shortformPreviewParagraphsWordCountThreshold: 30, shortformMinimumWordLengthForTruncation: 30}\n ) {\n bodyModel {\n ...PostBody_bodyModel\n __typename\n }\n isFullContent\n __typename\n }\n __typename\n id\n}\n\nfragment PostBody_bodyModel on RichText {\n sections {\n name\n startIndex\n textLayout\n imageLayout\n backgroundImage {\n id\n originalHeight\n originalWidth\n __typename\n }\n videoLayout\n backgroundVideo {\n videoId\n originalHeight\n originalWidth\n previewImageId\n __typename\n }\n __typename\n }\n paragraphs {\n id\n ...PostBodySection_paragraph\n __typename\n }\n ...normalizedBodyModel_richText\n __typename\n}\n\nfragment PostBodySection_paragraph on Paragraph {\n name\n ...PostBodyParagraph_paragraph\n __typename\n id\n}\n\nfragment PostBodyParagraph_paragraph on Paragraph {\n name\n type\n ...ImageParagraph_paragraph\n ...TextParagraph_paragraph\n ...IframeParagraph_paragraph\n ...MixtapeParagraph_paragraph\n __typename\n id\n}\n\nfragment ImageParagraph_paragraph on Paragraph {\n href\n layout\n metadata {\n id\n originalHeight\n originalWidth\n focusPercentX\n focusPercentY\n alt\n __typename\n }\n ...Markups_paragraph\n ...ParagraphRefsMapContext_paragraph\n ...PostAnnotationsMarker_paragraph\n __typename\n id\n}\n\nfragment Markups_paragraph on Paragraph {\n name\n text\n hasDropCap\n dropCapImage {\n ...MarkupNode_data_dropCapImage\n __typename\n id\n }\n markups {\n type\n start\n end\n href\n anchorType\n userId\n linkMetadata {\n httpStatus\n __typename\n }\n __typename\n }\n __typename\n id\n}\n\nfragment MarkupNode_data_dropCapImage on ImageMetadata {\n ...DropCap_image\n __typename\n id\n}\n\nfragment DropCap_image on ImageMetadata {\n id\n originalHeight\n originalWidth\n __typename\n}\n\nfragment ParagraphRefsMapContext_paragraph on Paragraph {\n id\n name\n text\n __typename\n}\n\nfragment PostAnnotationsMarker_paragraph on Paragraph {\n ...PostViewNoteCard_paragraph\n __typename\n id\n}\n\nfragment PostViewNoteCard_paragraph on Paragraph {\n name\n __typename\n id\n}\n\nfragment TextParagraph_paragraph on Paragraph {\n type\n hasDropCap\n ...Markups_paragraph\n ...ParagraphRefsMapContext_paragraph\n __typename\n id\n}\n\nfragment IframeParagraph_paragraph on Paragraph {\n iframe {\n mediaResource {\n id\n iframeSrc\n iframeHeight\n iframeWidth\n title\n __typename\n }\n __typename\n }\n layout\n ...getEmbedlyCardUrlParams_paragraph\n ...Markups_paragraph\n __typename\n id\n}\n\nfragment getEmbedlyCardUrlParams_paragraph on Paragraph {\n type\n iframe {\n mediaResource {\n iframeSrc\n __typename\n }\n __typename\n }\n __typename\n id\n}\n\nfragment MixtapeParagraph_paragraph on Paragraph {\n type\n mixtapeMetadata {\n href\n mediaResource {\n mediumCatalog {\n id\n __typename\n }\n __typename\n }\n __typename\n }\n ...GenericMixtapeParagraph_paragraph\n __typename\n id\n}\n\nfragment GenericMixtapeParagraph_paragraph on Paragraph {\n text\n mixtapeMetadata {\n href\n thumbnailImageId\n __typename\n }\n markups {\n start\n end\n type\n href\n __typename\n }\n __typename\n id\n}\n\nfragment normalizedBodyModel_richText on RichText {\n paragraphs {\n markups {\n type\n __typename\n }\n ...getParagraphHighlights_paragraph\n ...getParagraphPrivateNotes_paragraph\n __typename\n }\n sections {\n startIndex\n ...getSectionEndIndex_section\n __typename\n }\n ...getParagraphStyles_richText\n ...getParagraphSpaces_richText\n __typename\n}\n\nfragment getParagraphHighlights_paragraph on Paragraph {\n name\n __typename\n id\n}\n\nfragment getParagraphPrivateNotes_paragraph on Paragraph {\n name\n __typename\n id\n}\n\nfragment getSectionEndIndex_section on Section {\n startIndex\n __typename\n}\n\nfragment getParagraphStyles_richText on RichText {\n paragraphs {\n text\n type\n __typename\n }\n sections {\n ...getSectionEndIndex_section\n __typename\n }\n __typename\n}\n\nfragment getParagraphSpaces_richText on RichText {\n paragraphs {\n layout\n metadata {\n originalHeight\n originalWidth\n __typename\n }\n type\n ...paragraphExtendsImageGrid_paragraph\n __typename\n }\n ...getSeriesParagraphTopSpacings_richText\n ...getPostParagraphTopSpacings_richText\n __typename\n}\n\nfragment paragraphExtendsImageGrid_paragraph on Paragraph {\n layout\n type\n __typename\n id\n}\n\nfragment getSeriesParagraphTopSpacings_richText on RichText {\n paragraphs {\n id\n __typename\n }\n sections {\n startIndex\n __typename\n }\n __typename\n}\n\nfragment getPostParagraphTopSpacings_richText on RichText {\n paragraphs {\n layout\n text\n __typename\n }\n sections {\n startIndex\n __typename\n }\n __typename\n}\n\nfragment CardByline_post on Post {\n ...DraftStatus_post\n __typename\n id\n}\n\nfragment DraftStatus_post on Post {\n id\n pendingCollection {\n id\n creator {\n id\n __typename\n }\n ...BoldCollectionName_collection\n __typename\n }\n statusForCollection\n creator {\n id\n __typename\n }\n isPublished\n __typename\n}\n\nfragment BoldCollectionName_collection on Collection {\n id\n name\n __typename\n}\n\nfragment ExpandablePostFooter_post on Post {\n id\n allowResponses\n postResponses {\n count\n __typename\n }\n isLimitedState\n ...ExpandablePostCardOverflowButton_post\n ...BookmarkButton_post\n ...PostFooterSocialPopover_post\n ...MultiVote_post\n ...OverflowMenuButtonWithNegativeSignal_post\n __typename\n}\n\nfragment ExpandablePostCardOverflowButton_post on Post {\n creator {\n id\n __typename\n }\n ...ExpandablePostCardEditorWriterButton_post\n ...ExpandablePostCardReaderButton_post\n __typename\n id\n}\n\nfragment ExpandablePostCardEditorWriterButton_post on Post {\n id\n collection {\n id\n name\n slug\n __typename\n }\n allowResponses\n clapCount\n visibility\n mediumUrl\n responseDistribution\n ...useIsPinnedInContext_post\n ...CopyFriendLinkMenuItem_post\n ...NewsletterV3EmailToSubscribersMenuItem_post\n ...OverflowMenuItemUndoClaps_post\n __typename\n}\n\nfragment useIsPinnedInContext_post on Post {\n id\n collection {\n id\n __typename\n }\n pendingCollection {\n id\n __typename\n }\n pinnedAt\n pinnedByCreatorAt\n __typename\n}\n\nfragment CopyFriendLinkMenuItem_post on Post {\n id\n __typename\n}\n\nfragment NewsletterV3EmailToSubscribersMenuItem_post on Post {\n id\n creator {\n id\n newsletterV3 {\n id\n subscribersCount\n __typename\n }\n __typename\n }\n isNewsletter\n isAuthorNewsletter\n __typename\n}\n\nfragment OverflowMenuItemUndoClaps_post on Post {\n id\n clapCount\n ...ClapMutation_post\n __typename\n}\n\nfragment ClapMutation_post on Post {\n __typename\n id\n clapCount\n ...MultiVoteCount_post\n}\n\nfragment MultiVoteCount_post on Post {\n id\n ...PostVotersNetwork_post\n __typename\n}\n\nfragment PostVotersNetwork_post on Post {\n id\n voterCount\n recommenders {\n name\n __typename\n }\n __typename\n}\n\nfragment ExpandablePostCardReaderButton_post on Post {\n id\n collection {\n id\n __typename\n }\n creator {\n id\n __typename\n }\n clapCount\n ...ClapMutation_post\n __typename\n}\n\nfragment BookmarkButton_post on Post {\n visibility\n ...SusiClickable_post\n ...AddToCatalogBookmarkButton_post\n __typename\n id\n}\n\nfragment SusiClickable_post on Post {\n id\n mediumUrl\n ...SusiContainer_post\n __typename\n}\n\nfragment SusiContainer_post on Post {\n id\n __typename\n}\n\nfragment AddToCatalogBookmarkButton_post on Post {\n ...AddToCatalogBase_post\n __typename\n id\n}\n\nfragment AddToCatalogBase_post on Post {\n id\n __typename\n}\n\nfragment PostFooterSocialPopover_post on Post {\n id\n mediumUrl\n title\n ...SharePostButton_post\n __typename\n}\n\nfragment SharePostButton_post on Post {\n id\n __typename\n}\n\nfragment MultiVote_post on Post {\n id\n clapCount\n creator {\n id\n ...SusiClickable_user\n __typename\n }\n isPublished\n ...SusiClickable_post\n collection {\n id\n slug\n __typename\n }\n isLimitedState\n ...MultiVoteCount_post\n __typename\n}\n\nfragment OverflowMenuButtonWithNegativeSignal_post on Post {\n id\n ...OverflowMenuWithNegativeSignal_post\n ...CreatorActionOverflowPopover_post\n __typename\n}\n\nfragment OverflowMenuWithNegativeSignal_post on Post {\n id\n creator {\n id\n __typename\n }\n collection {\n id\n __typename\n }\n ...OverflowMenuItemUndoClaps_post\n __typename\n}\n\nfragment CreatorActionOverflowPopover_post on Post {\n allowResponses\n id\n statusForCollection\n isLocked\n isPublished\n clapCount\n mediumUrl\n pinnedAt\n pinnedByCreatorAt\n curationEligibleAt\n mediumUrl\n responseDistribution\n visibility\n inResponseToPostResult {\n __typename\n }\n inResponseToCatalogResult {\n __typename\n }\n pendingCollection {\n id\n name\n creator {\n id\n __typename\n }\n avatar {\n id\n __typename\n }\n domain\n slug\n __typename\n }\n creator {\n id\n ...MutePopoverOptions_creator\n ...auroraHooks_publisher\n __typename\n }\n collection {\n id\n name\n creator {\n id\n __typename\n }\n avatar {\n id\n __typename\n }\n domain\n slug\n ...MutePopoverOptions_collection\n ...auroraHooks_publisher\n __typename\n }\n ...useIsPinnedInContext_post\n ...NewsletterV3EmailToSubscribersMenuItem_post\n ...OverflowMenuItemUndoClaps_post\n __typename\n}\n\nfragment auroraHooks_publisher on Publisher {\n __typename\n ... on Collection {\n isAuroraEligible\n isAuroraVisible\n viewerEdge {\n id\n isEditor\n __typename\n }\n __typename\n id\n }\n ... on User {\n isAuroraVisible\n __typename\n id\n }\n}\n\nfragment InResponseToEntityPreview_post on Post {\n id\n inResponseToEntityType\n __typename\n}\n\nfragment PostScrollTracker_post on Post {\n id\n collection {\n id\n __typename\n }\n sequence {\n sequenceId\n __typename\n }\n __typename\n}\n\nfragment ReadMore_post on Post {\n mediumUrl\n readingTime\n ...usePostUrl_post\n __typename\n id\n}\n\nfragment usePostUrl_post on Post {\n id\n creator {\n ...userUrl_user\n __typename\n id\n }\n collection {\n id\n domain\n slug\n __typename\n }\n isSeries\n mediumUrl\n sequence {\n slug\n __typename\n }\n uniqueSlug\n __typename\n}\n\nfragment HighDensityPreview_post on Post {\n id\n title\n previewImage {\n id\n focusPercentX\n focusPercentY\n __typename\n }\n extendedPreviewContent(\n truncationConfig: {previewParagraphsWordCountThreshold: 400, minimumWordLengthForTruncation: 150, truncateAtEndOfSentence: true, showFullImageCaptions: true, shortformPreviewParagraphsWordCountThreshold: 30, shortformMinimumWordLengthForTruncation: 30}\n ) {\n subtitle\n __typename\n }\n ...HighDensityFooter_post\n __typename\n}\n\nfragment HighDensityFooter_post on Post {\n id\n readingTime\n tags {\n ...TopicPill_tag\n __typename\n }\n ...BookmarkButton_post\n ...ExpandablePostCardOverflowButton_post\n ...OverflowMenuButtonWithNegativeSignal_post\n __typename\n}\n\nfragment TopicPill_tag on Tag {\n __typename\n id\n displayTitle\n}\n\nfragment CardByline_publisher on Publisher {\n __typename\n ... on User {\n id\n ...CardByline_user\n __typename\n }\n ... on Collection {\n id\n ...CardByline_collection\n __typename\n }\n}\n\nfragment NewsletterV3Promo_publisher on Publisher {\n __typename\n ... on User {\n ...NewsletterV3Promo_publisher_User\n __typename\n id\n }\n ... on Collection {\n ...NewsletterV3Promo_publisher_Collection\n __typename\n id\n }\n}\n\nfragment NewsletterV3Promo_publisher_User on User {\n id\n username\n name\n viewerIsUser\n newsletterV3 {\n id\n ...NewsletterV3Promo_newsletterV3\n __typename\n }\n __typename\n}\n\nfragment NewsletterV3Promo_newsletterV3 on NewsletterV3 {\n slug\n name\n description\n promoHeadline\n promoBody\n ...NewsletterV3AmpButton_newsletterV3\n ...NewsletterV3SubscribeButton_newsletterV3\n ...NewsletterV3SubscribeByEmail_newsletterV3\n __typename\n id\n}\n\nfragment NewsletterV3AmpButton_newsletterV3 on NewsletterV3 {\n id\n collection {\n ...collectionDefaultBackgroundTheme_collection\n __typename\n id\n }\n __typename\n}\n\nfragment collectionDefaultBackgroundTheme_collection on Collection {\n colorPalette {\n ...collectionDefaultBackgroundTheme_colorPalette\n __typename\n }\n customStyleSheet {\n id\n ...collectionDefaultBackgroundTheme_customStyleSheet\n __typename\n }\n __typename\n id\n}\n\nfragment collectionDefaultBackgroundTheme_colorPalette on ColorPalette {\n ...customDefaultBackgroundTheme_colorPalette\n __typename\n}\n\nfragment customDefaultBackgroundTheme_colorPalette on ColorPalette {\n highlightSpectrum {\n ...ThemeUtil_colorSpectrum\n __typename\n }\n defaultBackgroundSpectrum {\n ...ThemeUtil_colorSpectrum\n __typename\n }\n tintBackgroundSpectrum {\n ...ThemeUtil_colorSpectrum\n __typename\n }\n __typename\n}\n\nfragment collectionDefaultBackgroundTheme_customStyleSheet on CustomStyleSheet {\n id\n ...customDefaultBackgroundTheme_customStyleSheet\n __typename\n}\n\nfragment customDefaultBackgroundTheme_customStyleSheet on CustomStyleSheet {\n id\n global {\n colorPalette {\n primary {\n colorPalette {\n ...customDefaultBackgroundTheme_colorPalette\n __typename\n }\n __typename\n }\n background {\n colorPalette {\n ...customDefaultBackgroundTheme_colorPalette\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment NewsletterV3SubscribeButton_newsletterV3 on NewsletterV3 {\n id\n name\n slug\n type\n user {\n id\n name\n username\n __typename\n }\n collection {\n slug\n ...SusiClickable_collection\n ...collectionDefaultBackgroundTheme_collection\n __typename\n id\n }\n ...SusiClickable_newsletterV3\n ...useNewsletterV3Subscription_newsletterV3\n __typename\n}\n\nfragment SusiClickable_newsletterV3 on NewsletterV3 {\n ...SusiContainer_newsletterV3\n __typename\n id\n}\n\nfragment SusiContainer_newsletterV3 on NewsletterV3 {\n ...SignInOptions_newsletterV3\n ...SignUpOptions_newsletterV3\n __typename\n id\n}\n\nfragment SignInOptions_newsletterV3 on NewsletterV3 {\n id\n name\n __typename\n}\n\nfragment SignUpOptions_newsletterV3 on NewsletterV3 {\n id\n name\n __typename\n}\n\nfragment NewsletterV3SubscribeByEmail_newsletterV3 on NewsletterV3 {\n id\n slug\n type\n user {\n id\n name\n username\n __typename\n }\n collection {\n ...collectionDefaultBackgroundTheme_collection\n ...collectionUrl_collection\n __typename\n id\n }\n __typename\n}\n\nfragment NewsletterV3Promo_publisher_Collection on Collection {\n id\n slug\n domain\n name\n newsletterV3 {\n id\n ...NewsletterV3Promo_newsletterV3\n __typename\n }\n __typename\n}\n\nfragment PublisherHomePosts_user on User {\n id\n ...useShowAuthorNewsletterV3Promo_user\n __typename\n}\n\nfragment useShowAuthorNewsletterV3Promo_user on User {\n id\n username\n newsletterV3 {\n id\n showPromo\n slug\n __typename\n }\n __typename\n}\n\nfragment UserSubdomainFlow_user on User {\n id\n hasCompletedProfile\n name\n bio\n imageId\n ...UserCompleteProfileDialog_user\n ...UserSubdomainOnboardingDialog_user\n __typename\n}\n\nfragment UserCompleteProfileDialog_user on User {\n id\n name\n bio\n imageId\n hasCompletedProfile\n __typename\n}\n\nfragment UserSubdomainOnboardingDialog_user on User {\n id\n customDomainState {\n pending {\n status\n __typename\n }\n live {\n status\n __typename\n }\n __typename\n }\n username\n __typename\n}\n\nfragment UserProfileMetadata_user on User {\n id\n username\n name\n bio\n socialStats {\n followerCount\n followingCount\n __typename\n }\n ...userUrl_user\n ...UserProfileMetadataHelmet_user\n __typename\n}\n\nfragment UserProfileMetadataHelmet_user on User {\n username\n name\n imageId\n twitterScreenName\n navItems {\n title\n __typename\n }\n __typename\n id\n}\n\nfragment SuspendedBannerLoader_user on User {\n id\n isSuspended\n __typename\n}\n\nfragment useAnalytics_user on User {\n id\n imageId\n name\n username\n __typename\n}\n\nfragment EntityDrivenSubscriptionLandingPageScreen_writer on User {\n name\n imageId\n id\n username\n isPartnerProgramEnrolled\n referredMembershipCustomHeadline\n referredMembershipCustomBody\n customStyleSheet {\n ...CustomThemeProvider_customStyleSheet\n ...CustomBackgroundWrapper_customStyleSheet\n ...MetaHeader_customStyleSheet\n __typename\n id\n }\n ...MetaHeader_publisher\n ...userUrl_user\n __typename\n}\n\nfragment CustomThemeProvider_customStyleSheet on CustomStyleSheet {\n id\n ...customDefaultBackgroundTheme_customStyleSheet\n ...customStyleSheetFontTheme_customStyleSheet\n __typename\n}\n\nfragment customStyleSheetFontTheme_customStyleSheet on CustomStyleSheet {\n id\n global {\n fonts {\n font1 {\n name\n __typename\n }\n font2 {\n name\n __typename\n }\n font3 {\n name\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment CustomBackgroundWrapper_customStyleSheet on CustomStyleSheet {\n id\n global {\n colorPalette {\n background {\n ...getHexFromColorValue_colorValue\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment MetaHeader_customStyleSheet on CustomStyleSheet {\n id\n header {\n headerScale\n horizontalAlignment\n __typename\n }\n ...MetaHeaderBackground_customStyleSheet\n ...MetaHeaderEngagement_customStyleSheet\n ...MetaHeaderLogo_customStyleSheet\n ...MetaHeaderNavVertical_customStyleSheet\n ...MetaHeaderTagline_customStyleSheet\n ...MetaHeaderThemeProvider_customStyleSheet\n __typename\n}\n\nfragment MetaHeaderBackground_customStyleSheet on CustomStyleSheet {\n id\n header {\n headerScale\n backgroundImageDisplayMode\n backgroundImageVerticalAlignment\n backgroundColorDisplayMode\n backgroundColor {\n ...getHexFromColorValue_colorValue\n ...getOpaqueHexFromColorValue_colorValue\n __typename\n }\n secondaryBackgroundColor {\n ...getHexFromColorValue_colorValue\n __typename\n }\n postBackgroundColor {\n ...getHexFromColorValue_colorValue\n __typename\n }\n backgroundImage {\n ...MetaHeaderBackground_imageMetadata\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment MetaHeaderEngagement_customStyleSheet on CustomStyleSheet {\n ...MetaHeaderNav_customStyleSheet\n __typename\n id\n}\n\nfragment MetaHeaderNav_customStyleSheet on CustomStyleSheet {\n id\n navigation {\n navItems {\n ...MetaHeaderNav_headerNavigationItem\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment MetaHeaderNav_headerNavigationItem on HeaderNavigationItem {\n name\n tagSlugs\n ...MetaHeaderNavLink_headerNavigationItem\n __typename\n}\n\nfragment MetaHeaderNavLink_headerNavigationItem on HeaderNavigationItem {\n name\n ...getNavItemHref_headerNavigationItem\n __typename\n}\n\nfragment getNavItemHref_headerNavigationItem on HeaderNavigationItem {\n href\n type\n tags {\n id\n normalizedTagSlug\n __typename\n }\n __typename\n}\n\nfragment MetaHeaderLogo_customStyleSheet on CustomStyleSheet {\n id\n header {\n nameColor {\n ...getHexFromColorValue_colorValue\n __typename\n }\n nameTreatment\n postNameTreatment\n logoImage {\n ...MetaHeaderLogo_imageMetadata\n __typename\n }\n logoScale\n __typename\n }\n __typename\n}\n\nfragment MetaHeaderLogo_imageMetadata on ImageMetadata {\n id\n originalWidth\n originalHeight\n ...PublisherLogo_image\n __typename\n}\n\nfragment PublisherLogo_image on ImageMetadata {\n id\n originalHeight\n originalWidth\n __typename\n}\n\nfragment MetaHeaderNavVertical_customStyleSheet on CustomStyleSheet {\n id\n navigation {\n navItems {\n ...MetaHeaderNavLink_headerNavigationItem\n __typename\n }\n __typename\n }\n ...MetaHeaderNav_customStyleSheet\n __typename\n}\n\nfragment MetaHeaderTagline_customStyleSheet on CustomStyleSheet {\n id\n header {\n taglineColor {\n ...getHexFromColorValue_colorValue\n __typename\n }\n taglineTreatment\n __typename\n }\n __typename\n}\n\nfragment MetaHeaderThemeProvider_customStyleSheet on CustomStyleSheet {\n id\n ...useMetaHeaderTheme_customStyleSheet\n __typename\n}\n\nfragment useMetaHeaderTheme_customStyleSheet on CustomStyleSheet {\n ...customDefaultBackgroundTheme_customStyleSheet\n global {\n colorPalette {\n primary {\n colorPalette {\n tintBackgroundSpectrum {\n ...ThemeUtil_colorSpectrum\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n header {\n backgroundColor {\n colorPalette {\n tintBackgroundSpectrum {\n ...ThemeUtil_colorSpectrum\n __typename\n }\n __typename\n }\n __typename\n }\n postBackgroundColor {\n colorPalette {\n tintBackgroundSpectrum {\n ...ThemeUtil_colorSpectrum\n __typename\n }\n __typename\n }\n __typename\n }\n backgroundImage {\n id\n __typename\n }\n __typename\n }\n __typename\n id\n}\n\nfragment MetaHeader_publisher on Publisher {\n __typename\n name\n ...MetaHeaderEngagement_publisher\n ...MetaHeaderLogo_publisher\n ...MetaHeaderNavVertical_publisher\n ...MetaHeaderTagline_publisher\n ...MetaHeaderThemeProvider_publisher\n ...MetaHeaderActions_publisher\n ...MetaHeaderTop_publisher\n ...MetaHeaderNavLink_publisher\n ... on Collection {\n id\n favicon {\n id\n __typename\n }\n tagline\n ...CollectionNavigationContextProvider_collection\n __typename\n }\n ... on User {\n id\n bio\n ...UserProfileCatalogsLink_publisher\n __typename\n }\n}\n\nfragment MetaHeaderEngagement_publisher on Publisher {\n __typename\n ...MetaHeaderNav_publisher\n ...PublisherAboutLink_publisher\n ...PublisherFollowButton_publisher\n ...PublisherFollowerCount_publisher\n ... on Collection {\n creator {\n id\n __typename\n }\n customStyleSheet {\n id\n ...CustomThemeProvider_customStyleSheet\n __typename\n }\n __typename\n id\n }\n ... on User {\n ...UserProfileCatalogsLink_publisher\n ...UserSubscribeButton_user\n customStyleSheet {\n id\n ...CustomThemeProvider_customStyleSheet\n __typename\n }\n __typename\n id\n }\n}\n\nfragment MetaHeaderNav_publisher on Publisher {\n id\n ...MetaHeaderNavLink_publisher\n __typename\n}\n\nfragment MetaHeaderNavLink_publisher on Publisher {\n id\n ...getNavItemHref_publisher\n __typename\n}\n\nfragment getNavItemHref_publisher on Publisher {\n id\n ...publisherUrl_publisher\n __typename\n}\n\nfragment PublisherAboutLink_publisher on Publisher {\n __typename\n id\n ... on Collection {\n slug\n __typename\n id\n }\n ... on User {\n ...userUrl_user\n __typename\n id\n }\n}\n\nfragment PublisherFollowButton_publisher on Publisher {\n __typename\n ... on Collection {\n ...CollectionFollowButton_collection\n __typename\n id\n }\n ... on User {\n ...UserFollowButton_user\n __typename\n id\n }\n}\n\nfragment UserProfileCatalogsLink_publisher on Publisher {\n __typename\n id\n ... on User {\n ...userUrl_user\n homePostsPublished: homepagePostsConnection(paging: {limit: 1}) {\n posts {\n id\n __typename\n }\n __typename\n }\n __typename\n id\n }\n}\n\nfragment MetaHeaderLogo_publisher on Publisher {\n __typename\n id\n name\n ... on Collection {\n logo {\n ...MetaHeaderLogo_imageMetadata\n ...PublisherLogo_image\n __typename\n id\n }\n __typename\n id\n }\n ...auroraHooks_publisher\n}\n\nfragment MetaHeaderNavVertical_publisher on Publisher {\n id\n ...PublisherAboutLink_publisher\n ...MetaHeaderNav_publisher\n ...MetaHeaderNavLink_publisher\n __typename\n}\n\nfragment MetaHeaderTagline_publisher on Publisher {\n __typename\n ... on Collection {\n tagline\n __typename\n id\n }\n ... on User {\n bio\n __typename\n id\n }\n}\n\nfragment MetaHeaderThemeProvider_publisher on Publisher {\n __typename\n customStyleSheet {\n ...MetaHeaderThemeProvider_customStyleSheet\n __typename\n id\n }\n ... on Collection {\n colorPalette {\n ...customDefaultBackgroundTheme_colorPalette\n __typename\n }\n __typename\n id\n }\n}\n\nfragment MetaHeaderActions_publisher on Publisher {\n __typename\n ...MetaHeaderPubMenu_publisher\n ...SearchWidget_publisher\n ... on Collection {\n id\n creator {\n id\n __typename\n }\n customStyleSheet {\n navigation {\n navItems {\n name\n __typename\n }\n __typename\n }\n __typename\n id\n }\n ...CollectionAvatar_collection\n ...CollectionMetabarActionsPopover_collection\n ...MetaHeaderActions_collection_common\n __typename\n }\n ... on User {\n id\n ...UserAvatar_user\n __typename\n }\n}\n\nfragment SearchWidget_publisher on Publisher {\n __typename\n ... on Collection {\n id\n slug\n name\n domain\n __typename\n }\n ... on User {\n id\n name\n __typename\n }\n ...algoliaSearch_publisher\n}\n\nfragment algoliaSearch_publisher on Publisher {\n __typename\n id\n}\n\nfragment CollectionMetabarActionsPopover_collection on Collection {\n id\n slug\n isAuroraEligible\n isAuroraVisible\n newsletterV3 {\n id\n slug\n __typename\n }\n ...collectionUrl_collection\n __typename\n}\n\nfragment MetaHeaderActions_collection_common on Collection {\n creator {\n id\n __typename\n }\n __typename\n id\n}\n\nfragment MetaHeaderTop_publisher on Publisher {\n __typename\n ... on Collection {\n slug\n ...CollectionMetabarActionsPopover_collection\n ...CollectionAvatar_collection\n ...MetaHeaderTop_collection\n __typename\n id\n }\n ... on User {\n username\n id\n __typename\n }\n}\n\nfragment MetaHeaderTop_collection on Collection {\n id\n creator {\n id\n __typename\n }\n __typename\n}\n\nfragment CollectionNavigationContextProvider_collection on Collection {\n id\n domain\n slug\n isAuroraVisible\n __typename\n}\n\nfragment useShouldShowEntityDrivenSubscription_creator on User {\n id\n __typename\n}\n"
|
36
|
+
}
|
37
|
+
]
|
38
|
+
|
39
|
+
body = Request.body(Request.URL("https://medium.com/_/graphql", "POST", query))
|
40
|
+
json = JSON.parse(body)
|
41
|
+
|
42
|
+
nextInfo = json&.dig(0, "data", "userResult", "homepagePostsConnection", "pagingInfo", "next")
|
43
|
+
postsInfo = json&.dig(0, "data", "userResult", "homepagePostsConnection", "posts")
|
44
|
+
|
45
|
+
nextID = nil
|
46
|
+
postURLs = []
|
47
|
+
if !nextInfo.nil?
|
48
|
+
nextID = nextInfo["from"]
|
49
|
+
end
|
50
|
+
|
51
|
+
if !postsInfo.nil?
|
52
|
+
postURLs = postsInfo.map { |post| post["mediumUrl"] }
|
53
|
+
end
|
54
|
+
|
55
|
+
{
|
56
|
+
"nextID" => nextID,
|
57
|
+
"postURLs" => postURLs
|
58
|
+
}
|
59
|
+
end
|
60
|
+
end
|
metadata
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ZMediumToMarkdown
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- ZhgChgLi
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-05-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: nokogiri
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.13.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.13.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: reverse_markdown
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.1.1
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 2.1.1
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: net-http
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.1.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.1.0
|
55
|
+
description: ZMediumToMarkdown lets you download Medium post and convert it to markdown
|
56
|
+
format easily.
|
57
|
+
email:
|
58
|
+
executables:
|
59
|
+
- ZMediumFetcher
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- bin/ZMediumFetcher
|
64
|
+
- lib/ErrorHandle.rb
|
65
|
+
- lib/Helper.rb
|
66
|
+
- lib/ImageDownloader.rb
|
67
|
+
- lib/Models/Paragraph.rb
|
68
|
+
- lib/Parsers/BQParser.rb
|
69
|
+
- lib/Parsers/FallbackParser.rb
|
70
|
+
- lib/Parsers/H1Parser.rb
|
71
|
+
- lib/Parsers/H2Parser.rb
|
72
|
+
- lib/Parsers/H3Parser.rb
|
73
|
+
- lib/Parsers/H4Parser.rb
|
74
|
+
- lib/Parsers/IMGParser.rb
|
75
|
+
- lib/Parsers/IframeParser.rb
|
76
|
+
- lib/Parsers/LinkParser.rb
|
77
|
+
- lib/Parsers/MIXTAPEEMBEDParser.rb
|
78
|
+
- lib/Parsers/MarkupParser.rb
|
79
|
+
- lib/Parsers/OLIParser.rb
|
80
|
+
- lib/Parsers/PParser.rb
|
81
|
+
- lib/Parsers/PQParser.rb
|
82
|
+
- lib/Parsers/PREParser.rb
|
83
|
+
- lib/Parsers/Parser.rb
|
84
|
+
- lib/Parsers/ULIParser.rb
|
85
|
+
- lib/PathPolicy.rb
|
86
|
+
- lib/Post.rb
|
87
|
+
- lib/Request.rb
|
88
|
+
- lib/User.rb
|
89
|
+
homepage: https://github.com/ZhgChgLi/ZMediumToMarkdown
|
90
|
+
licenses:
|
91
|
+
- MIT
|
92
|
+
metadata: {}
|
93
|
+
post_install_message:
|
94
|
+
rdoc_options: []
|
95
|
+
require_paths:
|
96
|
+
- lib
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
requirements: []
|
108
|
+
rubygems_version: 3.0.3
|
109
|
+
signing_key:
|
110
|
+
specification_version: 4
|
111
|
+
summary: This project can help you to make an auto-sync or auto-backup service from
|
112
|
+
Medium, like auto-sync Medium posts to Jekyll or other static markdown blog engines
|
113
|
+
or auto-backup Medium posts to the Github page.
|
114
|
+
test_files: []
|