ZMediumToMarkdown 1.0.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 +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
|
+
""
|
24
|
+
else
|
25
|
+
""
|
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[](#{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: []
|