ZMediumToMarkdown 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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'
@@ -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
@@ -0,0 +1,7 @@
1
+
2
+
3
+ class ErrorHandle
4
+ def self.error()
5
+
6
+ end
7
+ end
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,12 @@
1
+ $lib = File.expand_path('../', File.dirname(__FILE__))
2
+
3
+ require "Parsers/Parser"
4
+ require 'Models/Paragraph'
5
+
6
+ class FallbackParser < Parser
7
+ attr_accessor :nextParser
8
+ def parse(paragraph)
9
+ puts paragraph.type
10
+ "#{paragraph.text}"
11
+ end
12
+ 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,16 @@
1
+ $lib = File.expand_path('../lib', File.dirname(__FILE__))
2
+
3
+ class Parser
4
+ attr_accessor :nextParser
5
+ def initialize()
6
+ @nextParser = nil
7
+ end
8
+
9
+ def parse(paragraph)
10
+
11
+ end
12
+
13
+ def setNext(parser)
14
+ @nextParser = parser
15
+ end
16
+ 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: []