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 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: []