sutty-migration 0.3.5 → 0.4.0rc0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1fd9ce789787d3a77c484a988bc3a97d687d2db636ced588954578c2a069b60
4
- data.tar.gz: 848ae95f5a846a9ad8d7102a3bfbfc2c8813bf8f656e40bbae79a66eb41f482f
3
+ metadata.gz: 33b04f68ec23be8ad61143db87c0496fc1a01a84fdb3351b9f76a2782f79342f
4
+ data.tar.gz: 411e0050ee48d300c7bcda874f1943b33d867f4c090f72650c0122a69027faa2
5
5
  SHA512:
6
- metadata.gz: 1b3908174927eb8034605f60c56e40c2455514f1e4a707d425c1eab65dc892f9223ffa2eec055d97f4810795f7cf4599daf0803ae823c62eafb7cbc793d4b858
7
- data.tar.gz: 767de3717e12d798fba13af63c9f889e26e155228784b07faadfdb8aeddc88a5930437760305a146422366239debe3dd3f13e8b6935a1951aff89ac7008cb0c1
6
+ metadata.gz: 708dff82c233c3819acbe9ccf3901d8345463282ba9df953adb8ac0f15f9646560e514c87499921d13ac6bfa2693c71b0d634e03e1f04e87d0e148174bd91c50
7
+ data.tar.gz: 0faa1dc6c107f0999d8a52b7c28b08ad1540e44cff8fd488bfcfae55d2d22548fe0afe61fb54520f4b1d8552674079ba65d617a361139cbb1a5c761581e6df7f
data/README.md CHANGED
@@ -91,30 +91,31 @@ SQLite syntax, since `mysql2sqlite` doesn't support this yet.
91
91
  Wordpress websites can include lots of posts and metadata, depending on
92
92
  the amount of plugins installed. We don't have an official way of
93
93
  dumping everything into Jekyll, because you will probably want to move
94
- things around. You can write a plugin like this:
94
+ things around. You can write a plugin like this to inspect what's
95
+ happening on every stage:
95
96
 
96
97
  ```ruby
97
98
  # _plugins/wordpress.rb
98
99
  # frozen_string_literal: true
99
100
 
100
- require 'sutty_migration/wordpress'
101
- require 'sutty_migration/jekyll/document_creator'
102
- require 'jekyll-write-and-commit-changes'
103
-
104
- Jekyll::Hooks.register :site, :post_read, priority: :low do |site|
105
- wp = SuttyMigration::Wordpress.new(site: site, database: 'wordpress.sqlite3', prefix: 'wp_', url: 'https://wordpre.ss')
106
-
107
- # Download all files
108
- wp.download_all
101
+ require 'jekyll-wordpress-sql-migration'
102
+ require 'pry'
109
103
 
110
- wp.posts(layout: 'post').each do |post|
111
- doc = Jekyll::Document.create(site: site, title: post[:post_title], date: post[:post_date], collection: 'posts')
112
- doc.content = post[:content]
113
- doc.save
114
- end
104
+ Jekyll::Hooks.register :wordpress_post, :post_init do |item|
105
+ binding.pry
115
106
  end
116
107
  ```
117
108
 
109
+ Available hooks are `:wordpress_attachment`, `:wordpress_post` and
110
+ `:migration` for the `:post_init`, `:pre_render`, `:post_convert`,
111
+ `:post_render` and `:post_write` stages.
112
+
113
+ And then set `JEKYLL_ENV=migration` for the migration to run.
114
+
115
+ If the website is still available (although probably doesn't),
116
+ attachments will be downloaded to `wp-content/`. If you have them from
117
+ a backup, put them there so the plugin finds them.
118
+
118
119
  ### WordPress XML
119
120
 
120
121
  If you have the XML dump from a WordPress site, you can migrate content
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Enable this plugin if you have an SQL dump backup of your Wordpress
4
+ # site. To avoid having to run a MySQL/MariaDB database, we convert the
5
+ # dump into an SQLite database with our fork of
6
+ # [mysql2sqlite](https://git.sutty.abyaya.la/misc/mysql2sqlite)
7
+ #
8
+ # Put the SQLite file on _files/wordpress.sqlite3 and set the URL on the
9
+ # `url` field on `_config.yml`. If the new website doesn't match the
10
+ # URL, you can use `wordpress_url`.
11
+ #
12
+ # If the prefix for Wordpress tables is different than "wp_", set
13
+ # `wordpress_prefix` on `_config.yml`.
14
+ #
15
+ # ```sh
16
+ # JEKYLL_ENV=migration bundle exec jekyll build
17
+ # ```
18
+ #
19
+ # ## Hooks
20
+ #
21
+ # Hook to `:migration` if you need to access and modify data during the
22
+ # migration process:
23
+ #
24
+ # * `:post_init`: will receive the `Hash`
25
+ # * `:pre_render`: will receive the `SuttyMigration::WordpressXml::Post`
26
+ # * `:post_convert`: will receive the `Jekyll::Document` after setting front matter
27
+ # * `:post_render`: will receive the `Jekyll::Document` after sanitizing the content
28
+ # * `:post_write`: will receive the `Jekyll::Document` after writing to disk
29
+
30
+ Jekyll::Hooks.register :site, :post_read, priority: :high do |site|
31
+ next unless Jekyll.env == 'migration'
32
+
33
+ database = '_files/wordpress.sqlite3'
34
+
35
+ Jekyll.logger.abort_with 'Migration:', "#{database} doesn't exist!" unless File.exist? database
36
+
37
+ require 'sutty_migration/wordpress'
38
+ require 'sutty_migration/wordpress_xml/post'
39
+ require 'sutty_migration/wordpress_xml/attachment'
40
+ require 'sutty_migration/jekyll/document_creator'
41
+ require 'jekyll-write-and-commit-changes'
42
+
43
+ url = site.config['wordpress_url'] || site.config['url']
44
+ prefix = site.config['wordpress_prefix'] || 'wp_'
45
+
46
+ wordpress = SuttyMigration::Wordpress.new(site:, url:, database:, prefix:)
47
+ migrated_posts = []
48
+
49
+ Jekyll::Hooks.trigger :migration, :post_init, wordpress
50
+
51
+ wordpress.posts(layout: 'attachment').each do |item|
52
+ Jekyll::Hooks.trigger :wordpress_attachment, :post_init, item
53
+
54
+ attachment = SuttyMigration::WordpressXml::Attachment.new(wordpress:, item:)
55
+
56
+ # Download and convert to Sutty's public/
57
+ Jekyll::Hooks.trigger :wordpress_attachment, :pre_render, attachment
58
+ # Index attachments by id, public path and original destination so
59
+ # we can find them later.
60
+ wordpress.attachments[attachment.attachment_url] =
61
+ wordpress.attachments[attachment.public_path] =
62
+ wordpress.attachments[item[:id]] =
63
+ attachment
64
+ Jekyll::Hooks.trigger :wordpress_attachment, :post_convert, attachment
65
+ Jekyll::Hooks.trigger :wordpress_attachment, :post_render, attachment
66
+
67
+ Jekyll::Hooks.trigger :wordpress_attachment, :post_write, attachment
68
+ end
69
+
70
+ Jekyll::Hooks.trigger :migration, :pre_render, wordpress
71
+
72
+ # We have to instantiate Posts from a Hash instead of an XML object.
73
+ wordpress.posts(layout: 'post').each do |item|
74
+ Jekyll::Hooks.trigger :wordpress_post, :post_init, item
75
+
76
+ item[:title] = 'Draft' if item[:title].empty?
77
+ item[:slug] = Jekyll::Utils.slugify(item[:title], mode: 'latin') if item[:slug].empty?
78
+
79
+ post = SuttyMigration::WordpressXml::Post.new(wordpress:, item:).tap do |p|
80
+ p.categories = [item.dig(:terms, :category)].flatten.compact
81
+ p.tags = [item.dig(:terms, :post_tag)].flatten.compact
82
+ p.date = item[:date] || item[:last_modified_at]
83
+ p.last_modified_at = item[:last_modified_at]
84
+ end
85
+
86
+ Jekyll::Hooks.trigger :wordpress_post, :pre_render, post
87
+
88
+ Jekyll::Document.find_or_create(site:, slug: post.slug, title: post.title, date: post.date,
89
+ collection: 'posts').tap do |doc|
90
+ # If you want to customize the layout, change it with a :post_init
91
+ # hook.
92
+ doc.data['layout'] = item[:layout]
93
+ doc.data['created_at'] = post.date
94
+ doc.data['last_modified_at'] = post.last_modified_at&.localtime
95
+ doc.data['uuid'] ||= SecureRandom.uuid
96
+ doc.data['categories'] = post.categories
97
+ doc.data['tags'] = post.tags
98
+ doc.data['draft'] = post.draft?
99
+
100
+ unless (attachment_id = item.dig(:front_matter, :_thumbnail_id).to_i).zero?
101
+ attachment = wordpress.attachments[attachment_id]
102
+
103
+ if attachment
104
+ doc.data['image'] = {
105
+ 'path' => attachment.public_path,
106
+ 'description' => ''
107
+ }
108
+ end
109
+ end
110
+
111
+ Jekyll::Hooks.trigger :wordpress_post, :post_convert, doc
112
+
113
+ doc.content = post.beautiful_content
114
+
115
+ Jekyll::Hooks.trigger :wordpress_post, :post_render, doc
116
+
117
+ migrated_posts << doc
118
+ end
119
+ end
120
+
121
+ Jekyll::Hooks.trigger :migration, :post_convert, wordpress
122
+ Jekyll::Hooks.trigger :migration, :post_render, migrated_posts
123
+
124
+ # Save them all at the end
125
+ migrated_posts.each do |doc|
126
+ doc.save
127
+
128
+ Jekyll::Hooks.trigger :wordpress_post, :post_write, doc
129
+ end
130
+
131
+ Jekyll::Hooks.trigger :migration, :post_write, migrated_posts
132
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Enable this plugin if you have an XML backup of your Wordpress site.
4
+ # Although this is the easiest path, it's not always possible to have
5
+ # one, either because the website is not working or producing the XML
6
+ # dump gets canceled by PHP resource usage limits.
7
+ #
8
+ # Put the XML file on _files/wordpress.xml and set the URL on the `url`
9
+ # field on _config.yml. If the new website doesn't match the URL, you
10
+ # can use `wordpress_url`.
11
+ #
12
+ # JEKYLL_ENV=migration bundle exec jekyll build
13
+ Jekyll::Hooks.register :site, :post_read, priority: :high do |_site|
14
+ next unless Jekyll.env == 'migration'
15
+
16
+ wordpress_xml = '_files/wordpress.xml'
17
+
18
+ Jekyll.logger.abort_with 'Migration:', "#{wordpress_xml} is missing!" unless File.exists wordpress_xml
19
+ end
@@ -27,9 +27,9 @@ Jekyll::Hooks.register :site, :post_read, priority: :low do |site|
27
27
  end
28
28
 
29
29
  document ||= begin
30
- data = row.slice(*%w[date slug title]).transform_keys(&:to_sym)
31
- Jekyll::Document.find_or_create(site: site, collection: 'posts', **data)
32
- end
30
+ data = row.slice(*%w[date slug title]).transform_keys(&:to_sym)
31
+ Jekyll::Document.find_or_create(site: site, collection: 'posts', **data)
32
+ end
33
33
  next unless document
34
34
 
35
35
  row.each do |attribute, value|
@@ -17,7 +17,7 @@ module SuttyMigration
17
17
  # migration plugin where you can convert data by yourself. We may add
18
18
  # this feature in the future.
19
19
  class Wordpress
20
- attr_reader :site, :prefix, :limit, :url, :wp, :database, :multisite
20
+ attr_reader :site, :prefix, :limit, :url, :wp, :database, :multisite, :attachments
21
21
 
22
22
  # @param :site [Jekyll::Site] Jekyll site
23
23
  # @param :url [String] Wordpress site URL (must be up for downloads)
@@ -32,13 +32,14 @@ module SuttyMigration
32
32
  @url = url.freeze
33
33
  @database = database || File.join(site.source, '_data', 'wordpress.sqlite3')
34
34
  @multisite = multisite
35
+ @attachments = {}
35
36
  end
36
37
 
37
38
  # Generate database connections for a multisite WP
38
39
  #
39
40
  # @return [Hash] { "ID" => SuttyMigration::Wordpress }
40
41
  def blogs
41
- @blogs ||= wp["select * from #{prefix}blogs"].to_a.map do |blog|
42
+ @blogs ||= wp["select * from #{prefix}blogs"].to_a.to_h do |blog|
42
43
  url = "https://#{blog[:domain]}#{blog[:path]}"
43
44
  pfx = "#{prefix}#{blog[:blog_id]}_" if blog[:blog_id] > 1
44
45
  pfx ||= prefix
@@ -46,11 +47,11 @@ module SuttyMigration
46
47
  [blog[:blog_id],
47
48
  blog.merge(db: self.class.new(site: site, url: url, prefix: pfx, database: database, limit: limit,
48
49
  multisite: self))]
49
- end.to_h
50
+ end
50
51
  end
51
52
 
52
53
  def options
53
- @options ||= wp["select option_name, option_value from #{prefix}options"].to_a.map(&:values).to_h.transform_keys(&:to_sym)
54
+ @options ||= wp["select option_name, option_value from #{prefix}options"].to_a.to_h(&:values).transform_keys(&:to_sym)
54
55
  end
55
56
 
56
57
  # Open the database.
@@ -62,53 +63,6 @@ module SuttyMigration
62
63
  end
63
64
  end
64
65
 
65
- # Download all attachments. Adds the local path to them.
66
- #
67
- # @param :progress [Boolean] Toggle progress bar
68
- # @return [Nil]
69
- def download_all(progress: true)
70
- posts(layout: 'attachment').each do |attachment|
71
- attachment[:front_matter]['file_path'] = download(url: attachment[:guid], progress: progress)
72
- end
73
- end
74
-
75
- # Downloads a file if needed, optionally showing a progress bar.
76
- #
77
- # @param :url [String] File URL
78
- # @param :progress [Boolean] Toggle progress bar
79
- # @return [String] File local path
80
- def download(url:, progress: true)
81
- uri = URI(url)
82
- dest = uri.path.sub(%r{\A/}, '')
83
- full = File.join(site.source, dest)
84
-
85
- return dest if File.exist? full
86
-
87
- ::Jekyll.logger.info "Downloading #{dest}"
88
-
89
- FileUtils.mkdir_p File.dirname(full)
90
-
91
- File.open(full, 'w') do |f|
92
- if progress
93
- head = Faraday.head(url)
94
- content_length = head.headers['content-length'].to_i
95
- progress = ProgressBar.create(title: File.basename(dest), total: content_length, output: $stderr)
96
- end
97
-
98
- Faraday.get(url) do |req|
99
- req.options.on_data = proc do |chunk, downloaded_bytes|
100
- f.write chunk
101
-
102
- if progress
103
- progress.progress = downloaded_bytes > content_length ? content_length : downloaded_bytes
104
- end
105
- end
106
- end
107
- end
108
-
109
- dest
110
- end
111
-
112
66
  # List post types
113
67
  #
114
68
  # @return [Array]
@@ -129,7 +83,7 @@ module SuttyMigration
129
83
  end
130
84
 
131
85
  wp[post_query(**options)].each_page(limit).to_a.map(&:to_a).flatten.tap do |p|
132
- p.map do |post|
86
+ p.each do |post|
133
87
  # Sequel parses dates on localtime
134
88
  post[:date] = ::Jekyll::Utils.parse_date(post[:date]) unless post[:date].blank?
135
89
  unless post[:last_modified_at].blank?
@@ -4,6 +4,7 @@ require_relative 'post'
4
4
  require 'php-serialize'
5
5
  require 'faraday'
6
6
  require 'progressbar'
7
+ require 'addressable'
7
8
 
8
9
  module SuttyMigration
9
10
  class WordpressXml
@@ -13,14 +14,14 @@ module SuttyMigration
13
14
  #
14
15
  # @return [String]
15
16
  def attachment_url
16
- @attachment_url ||= attribute_value 'attachment_url'
17
+ @attachment_url ||= attribute_value('attachment_url') || attribute_value('guid')
17
18
  end
18
19
 
19
20
  # File destination
20
21
  #
21
22
  # @return [String]
22
23
  def dest
23
- @dest ||= CGI.unescape(URI(attachment_url).path.sub(%r{\A/}, ''))
24
+ @dest ||= CGI.unescape(Addressable::URI.parse(attachment_url).path.sub(%r{\A/}, '').unicode_normalize)
24
25
  end
25
26
 
26
27
  # Metadata, with file information as a Hash
@@ -45,25 +46,85 @@ module SuttyMigration
45
46
  FileUtils.mkdir_p File.dirname(dest)
46
47
 
47
48
  File.open(dest, 'w') do |f|
49
+ head = Faraday.head(attachment_url)
50
+
51
+ if head.status > 399
52
+ ::Jekyll.logger.warn 'Error:', head.status.to_s
53
+ return false
54
+ end
55
+
56
+ content_length = head.headers['content-length'].to_i
57
+
58
+ if content_length.zero?
59
+ ::Jekyll.logger.warn 'Error:', 'Content length is zero'
60
+ return false
61
+ end
62
+
48
63
  if progress
49
- head = Faraday.head(attachment_url)
50
- content_length = head.headers['content-length'].to_i
51
- progress = ProgressBar.create(title: File.basename(dest), total: content_length, output: $stderr)
64
+ progress_bar = ProgressBar.create(title: File.basename(dest), total: content_length,
65
+ output: $stderr)
52
66
  end
53
67
 
54
- Faraday.get(attachment_url) do |req|
55
- req.options.on_data = proc do |chunk, downloaded_bytes|
56
- f.write chunk
68
+ begin
69
+ Faraday.get(attachment_url) do |req|
70
+ req.options.on_data = proc do |chunk, downloaded_bytes|
71
+ if downloaded_bytes > content_length
72
+ raise Faraday::Error, 'File is bigger than informed, aborting download'
73
+ end
57
74
 
58
- if progress
59
- progress.progress = downloaded_bytes > content_length ? content_length : downloaded_bytes
75
+ f.write chunk
76
+
77
+ progress_bar.progress = downloaded_bytes if progress
60
78
  end
61
79
  end
80
+ rescue Faraday::Error => e
81
+ ::Jekyll.logger.warn 'Error:', e.message
62
82
  end
63
83
  end
64
84
 
85
+ progress_bar.stop if progress
65
86
  File.exist? dest
66
87
  end
88
+
89
+ # Local path following Sutty's conventions. Instead of producing
90
+ # a SecureRandom.base36 key, we use the first 28 characters of the
91
+ # digest to obtain constand names.
92
+ #
93
+ # It then hardlinks the original file.
94
+ #
95
+ # @return [String]
96
+ def public_path
97
+ @public_path ||= File.join('public', digest[..27], dest.split('/').last).tap do
98
+ next if File.exist? _1
99
+
100
+ # If the directory exists and it's not empty, but the file is
101
+ # not there, we either have the same file with different names
102
+ # or old thumbnails.
103
+ dir = File.dirname(_1)
104
+ if File.directory?(dir) && !Dir.empty?(dir)
105
+ Jekyll::Logger.warn 'Migration:',
106
+ "#{dir} is not empty, you probably have the same file with different names"
107
+ end
108
+
109
+ FileUtils.mkdir_p(File.dirname(_1))
110
+ FileUtils.ln(dest, _1)
111
+ end
112
+ end
113
+
114
+ # Download and produce a digest. Produce an error if the file
115
+ # can't be downloaded.
116
+ #
117
+ # @return [String]
118
+ def digest
119
+ @digest ||=
120
+ begin
121
+ require 'digest'
122
+
123
+ raise Faraday::Error unless download
124
+
125
+ Digest::SHA2.file(dest).hexdigest
126
+ end
127
+ end
67
128
  end
68
129
  end
69
130
  end
@@ -1,18 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'rails-html-sanitizer'
3
4
  require 'wordpress_formatting/wpautop'
5
+ require 'htmlbeautifier'
4
6
  require 'jekyll/utils'
7
+ require 'nokogiri'
5
8
 
6
9
  module SuttyMigration
7
10
  class WordpressXml
8
11
  # Represents a WordPress post
9
12
  class Post
10
13
  attr_reader :wordpress, :item
14
+ attr_writer :tags, :categories, :author, :meta, :date, :last_modified_at
11
15
 
16
+ SANITIZER = Rails::HTML::Sanitizer.safe_list_sanitizer.new
17
+ # This follows https://www.npmjs.com/package/@suttyweb/editor and
18
+ # also allows some known WP tags, you can add your own during
19
+ # plugin initilization.
20
+ #
21
+ # rubocop:disable Style/MutableConstant
22
+ TAGS = %w[a aside audio blockquote br caption code del em figcaption figure h1 h2 h3 h4 h5 h6 hr iframe img li mark ol p
23
+ pre small strong sub sup table tbody td tfoot th thead tr u ul video]
24
+ ATTRIBUTES = %w[alt colspan controls data-align data-id data-lang data-multimedia data-multimedia-inner
25
+ data-turbo href id name referrerpolicy rel reversed rowspan src start target title type]
26
+ # rubocop:enable Style/MutableConstant
12
27
  SLASH = '/'
28
+ THUMBNAIL_PREFIX = /(-scaled)?(-\d+x\d+)?(\.....?)\z/
29
+ # TODO: Move to wordpress-formatting
30
+ WORDPRESS_ELEMENT_START = /\[(\w+ [^\]]+)\]/
31
+ WORDPRESS_ELEMENT_END = %r{\[(/\w+)\]}
13
32
 
14
33
  # @param :wordpress [SuttyMigration::WordpressXml]
15
- # @param :item [Nokogiri::XML::Element]
34
+ # @param :item [Nokogiri::XML::Element, Hash]
16
35
  def initialize(wordpress:, item:)
17
36
  @wordpress = wordpress
18
37
  @item = item
@@ -35,13 +54,13 @@ module SuttyMigration
35
54
  def permalink
36
55
  @permalink ||=
37
56
  begin
38
- p = attribute_value('link').sub(wordpress.url, '')
57
+ p = attribute_value('link')&.delete(wordpress.url)
39
58
 
40
- if !p.end_with?(SLASH) && File.extname(p).empty?
41
- p << SLASH
42
- end
59
+ unless p.nil?
60
+ p << SLASH if !p.end_with?(SLASH) && File.extname(p).empty?
43
61
 
44
- p.squeeze(SLASH)
62
+ p.squeeze(SLASH)
63
+ end
45
64
  end
46
65
  end
47
66
 
@@ -63,7 +82,9 @@ module SuttyMigration
63
82
  #
64
83
  # @return [String]
65
84
  def slug
66
- @slug ||= attribute_value('post_name').tr(SLASH, '-').squeeze('-').sub(/\A-+/, '').sub(/-+\z/, '')
85
+ @slug ||= (attribute_value('post_name') || attribute_value('slug')).to_s.tr(SLASH, '-').squeeze('-').sub(/\A-+/, '').sub(
86
+ /-+\z/, ''
87
+ )
67
88
  end
68
89
 
69
90
  # Publication date.
@@ -89,13 +110,80 @@ module SuttyMigration
89
110
  end
90
111
  end
91
112
 
92
- # Content as HTML, with site URL removed.
113
+ # Content as an HTML string, converting Wordpress [tags] to <tags>
93
114
  #
94
115
  # @return [String]
95
116
  def content
96
- @content ||= WordpressFormatting::Wpautop.wpautop(attribute_value('encoded')).gsub(
97
- / (href|src)="#{wordpress.url}/, ' \\1="'
98
- )
117
+ @content ||= WordpressFormatting::Wpautop.wpautop((attribute_value('encoded') || attribute_value('content')).gsub(WORDPRESS_ELEMENT_START, '<\\1>').gsub(
118
+ WORDPRESS_ELEMENT_END, '<\\1>'
119
+ ))
120
+ end
121
+
122
+ # Remove extraneous code
123
+ #
124
+ # @return [String]
125
+ def sanitized_content
126
+ @sanitized_content ||= SANITIZER.sanitize(content.to_s, tags: TAGS, attributes: ATTRIBUTES)
127
+ end
128
+
129
+ # Returns a fragment that can be queried using Nokogiri and
130
+ # transformed for final migration. Applies common transformations
131
+ # too.
132
+ #
133
+ # @return [Nokogiri::HTML]
134
+ def parsed_content
135
+ @parsed_content ||= Nokogiri::HTML.fragment(sanitized_content).tap do |html|
136
+ html.css('[src]').each do |element|
137
+ # Remove the thumbnail suffix, we only want the original files
138
+ element['src'] = element['src'].sub(THUMBNAIL_PREFIX, '\\3')
139
+
140
+ if element['src'].start_with? wordpress.url
141
+ element['src'] = wordpress.attachments[CGI.unescape element['src']].public_path
142
+
143
+ # Move the element into a <figure> like our editor does
144
+ unless element.parent && element.parent.name == '#document-fragment'
145
+ parent = element.parent
146
+
147
+ parent = parent.parent until parent.parent.nil? || parent.parent.name == '#document-fragment'
148
+
149
+ figure = parent.add_previous_sibling('<figure></figure>').first
150
+ figure.add_child element
151
+ # This was the [caption] WP element
152
+ figcaption = html.css('caption').first
153
+
154
+ if figcaption
155
+ figcaption.name = 'figcaption'
156
+ figure.add_child figcaption
157
+ element['alt'] = figcaption.text.strip
158
+ end
159
+ end
160
+
161
+ # Iframes are converted to an autolinked paragraph so
162
+ # jekyll-embed-urls can pick them up during build
163
+ elsif element.name == 'iframe'
164
+ element.parent.name = 'p'
165
+ element.name = 'a'
166
+ element['href'] = element['src']
167
+ if element['href'].include? 'youtube.com/embed/'
168
+ element['href'] = Addressable::URI.parse(element['href']).tap do |uri|
169
+ uri.query = URI.encode_www_form(URI.decode_www_form(uri.query.to_s).tap do |query|
170
+ query << ['v', uri.path.split('/').last]
171
+ end)
172
+ uri.path = '/watch'
173
+ end.to_s
174
+ end
175
+ element.delete 'src'
176
+ element.inner_html = element['href']
177
+ end
178
+ end
179
+ end
180
+ end
181
+
182
+ # Beautifies the parsed content.
183
+ #
184
+ # @return [String]
185
+ def beautiful_content
186
+ HtmlBeautifier.beautify(parsed_content.to_s.squeeze(' ').gsub(/>\s+</, '><'))
99
187
  end
100
188
 
101
189
  # Author attributes.
@@ -176,9 +264,16 @@ module SuttyMigration
176
264
 
177
265
  # Get a value from the attribute
178
266
  #
267
+ # @param key [String, Symbol]
179
268
  # @return [String, nil]
180
269
  def attribute_value(key)
181
- item.at_css(key)&.text
270
+ (case item
271
+ when Hash
272
+ item[key.to_sym]
273
+ else
274
+ item.at_css(key.to_s)&.text
275
+ end
276
+ )&.unicode_normalize&.strip
182
277
  end
183
278
  end
184
279
  end
@@ -117,18 +117,23 @@ module SuttyMigration
117
117
  #
118
118
  # @return [Hash]
119
119
  def pages
120
- @pages ||= items_find_by('post_type', 'page').map do |page|
121
- { attribute_value(page, 'post_id').to_i => Post.new(wordpress: self, item: page) }
122
- end.reduce(&:merge)
120
+ @pages ||= items_find_by('post_type', 'page').to_h do |item|
121
+ [attribute_value(page, 'post_id').to_i, Post.new(wordpress: self, item:)]
122
+ end
123
123
  end
124
124
 
125
- # Attachments, indexed by ID
125
+ # Attachments, indexed by URL and ID
126
126
  #
127
127
  # @return [Hash]
128
128
  def attachments
129
- @attachments ||= items_find_by('post_type', 'attachment').map do |attachment|
130
- { attribute_value(attachment, 'post_id').to_i => Attachment.new(wordpress: self, item: attachment) }
131
- end.reduce(&:merge)
129
+ @attachments ||= items_find_by('post_type', 'attachment').map do |item|
130
+ attachment = Attachment.new(wordpress: self, item:)
131
+
132
+ [
133
+ [attribute_value(item, 'post_id').to_i, attachment],
134
+ [attribute_value(item, 'attachment_url'), attachment]
135
+ ]
136
+ end.flatten(1).to_h
132
137
  end
133
138
 
134
139
  # Find items by attribute and value
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sutty-migration
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.5
4
+ version: 0.4.0rc0
5
5
  platform: ruby
6
6
  authors:
7
7
  - f
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-30 00:00:00.000000000 Z
11
+ date: 2026-10-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jekyll
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: addressable
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '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'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: fast_blank
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -150,6 +164,34 @@ dependencies:
150
164
  - - "~>"
151
165
  - !ruby/object:Gem::Version
152
166
  version: 1.3.0
167
+ - !ruby/object:Gem::Dependency
168
+ name: htmlbeautifier
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :runtime
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: rails-html-sanitizer
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :runtime
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
153
195
  - !ruby/object:Gem::Dependency
154
196
  name: pry
155
197
  requirement: !ruby/object:Gem::Requirement
@@ -164,7 +206,21 @@ dependencies:
164
206
  - - ">="
165
207
  - !ruby/object:Gem::Version
166
208
  version: '0'
167
- description: Takes datafiles and converts them into posts
209
+ - !ruby/object:Gem::Dependency
210
+ name: rubocop
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
223
+ description: Takes datafiles, Wordpress backups and converts them into Jekyll posts
168
224
  email:
169
225
  - f@sutty.nl
170
226
  executables: []
@@ -175,6 +231,8 @@ extra_rdoc_files:
175
231
  files:
176
232
  - LICENSE.txt
177
233
  - README.md
234
+ - lib/jekyll-wordpress-sql-migration.rb
235
+ - lib/jekyll-wordpress-xml-migration.rb
178
236
  - lib/sutty-migration.rb
179
237
  - lib/sutty_migration/core_extensions.rb
180
238
  - lib/sutty_migration/data.rb
@@ -183,19 +241,18 @@ files:
183
241
  - lib/sutty_migration/wordpress_xml.rb
184
242
  - lib/sutty_migration/wordpress_xml/attachment.rb
185
243
  - lib/sutty_migration/wordpress_xml/post.rb
186
- homepage: https://0xacab.org/sutty/jekyll/sutty-migration
244
+ homepage: https://git.sutty.abyaya.la/jekyll/sutty-migration
187
245
  licenses:
188
246
  - GPL-3.0
189
247
  metadata:
190
- bug_tracker_uri: https://0xacab.org/sutty/jekyll/sutty-migration/issues
191
- homepage_uri: https://0xacab.org/sutty/jekyll/sutty-migration
192
- source_code_uri: https://0xacab.org/sutty/jekyll/sutty-migration
193
- changelog_uri: https://0xacab.org/sutty/jekyll/sutty-migration/-/blob/master/CHANGELOG.md
248
+ bug_tracker_uri: https://git.sutty.abyaya.la/jekyll/sutty-migration/issues
249
+ homepage_uri: https://git.sutty.abyaya.la/jekyll/sutty-migration
250
+ source_code_uri: https://git.sutty.abyaya.la/jekyll/sutty-migration
194
251
  documentation_uri: https://rubydoc.info/gems/sutty-migration
195
252
  post_install_message:
196
253
  rdoc_options:
197
254
  - "--title"
198
- - sutty-migration - Jekyll data migration for Sutty
255
+ - sutty-migration - CSV, Wordpress SQL and Wordpress XML migration
199
256
  - "--main"
200
257
  - README.md
201
258
  - "--line-numbers"
@@ -207,15 +264,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
207
264
  requirements:
208
265
  - - ">="
209
266
  - !ruby/object:Gem::Version
210
- version: 2.6.0
267
+ version: '3.1'
211
268
  required_rubygems_version: !ruby/object:Gem::Requirement
212
269
  requirements:
213
- - - ">="
270
+ - - ">"
214
271
  - !ruby/object:Gem::Version
215
- version: '0'
272
+ version: 1.3.1
216
273
  requirements: []
217
274
  rubygems_version: 3.3.27
218
275
  signing_key:
219
276
  specification_version: 4
220
- summary: Jekyll data migration for Sutty
277
+ summary: CSV, Wordpress SQL and Wordpress XML migration
221
278
  test_files: []