diff --git a/CHANGELOG.md b/CHANGELOG.md index c515221..524fd63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,63 @@ A concise overview of the public-facing changes to the gem from version to versi ## Unreleased - Add Rake task `graphql-docs:generate` for integration with Rails and other Ruby projects that use Rake. Supports both task arguments and environment variables for configuration. Can be used with `Rake::Task["assets:precompile"].enhance(["graphql-docs:generate"])` to generate docs as part of the build process. +- **breaking:** Upgrade commonmarker, html-pipeline, and gemoji dependencies, see **BREAKING CHANGES** section below + +### 🚨 BREAKING CHANGES + +This release upgrades three major dependencies with significant breaking changes. + +#### Dependency Upgrades + +- **commonmarker**: `0.23.x` → `2.0.x` - Complete API rewrite with improved performance and standards compliance +- **html-pipeline**: `2.14.x` → `3.0.x` - Simplified architecture, filter API changed +- **gemoji**: `3.0.x` → `4.0.x` - Updated emoji mappings +- **Removed**: `extended-markdown-filter` (no longer maintained, incompatible with html-pipeline 3) + +#### Breaking Changes for Advanced Users + +1. **Custom html-pipeline filters no longer work** + - html-pipeline 3.x has a completely different filter API + - If you configured custom filters via `pipeline_config[:pipeline]`, they will not work + - **Migration**: Rewrite custom filters using html-pipeline 3.x API (see [html-pipeline migration guide](https://github.com/jch/html-pipeline/blob/main/CHANGELOG.md)) + - The gem now handles markdown and emoji rendering directly + +2. **Custom Renderer API changes** + - If you implemented a custom renderer that directly uses CommonMarker: + - **Old API**: `CommonMarker.render_html(string, :UNSAFE)` + - **New API**: `Commonmarker.parse(string).to_html(options: {render: {unsafe: true}})` + - Note the lowercase 'm' in `Commonmarker` in version 2.x + +3. **Table of Contents filter removed from defaults** + - The default `TableOfContentsFilter` is no longer applied + - **Migration**: Implement a custom post-processing step if needed + +#### What Still Works (and is Better!) + +- ✅ GitHub Flavored Markdown (tables, strikethrough, autolinks, task lists) +- ✅ **Emoji rendering** - `:emoji:` syntax like `:smile:` works out of the box +- ✅ Header anchors (automatically generated with IDs) +- ✅ Safe and unsafe HTML rendering modes +- ✅ Code blocks with syntax highlighting +- ✅ All existing templates and layouts +- ✅ Faster markdown rendering +- ✅ More standards-compliant HTML output + +#### Why Upgrade? + +- **Security**: Updates to latest stable versions with security patches +- **Performance**: commonmarker 2.x is significantly faster and more standards-compliant +- **Maintainability**: All dependencies are actively maintained +- **Modern**: Uses current Ruby ecosystem standards + +#### Migration Guide + +For most users with default configuration, this upgrade should be seamless. Advanced users should check: + +- [ ] Do you use custom `pipeline_config[:pipeline]` filters? → Rewrite for html-pipeline 3.x +- [ ] Do you have a custom `Renderer` subclass that calls CommonMarker directly? → Update API calls +- [ ] Run full test suite after upgrade +- [ ] Regenerate documentation and visually inspect output ## v5.2.0 - 2025-02-09 diff --git a/README.md b/README.md index d3aa319..615559d 100644 --- a/README.md +++ b/README.md @@ -272,7 +272,7 @@ generator.generate By default, the HTML generation process uses ERB to layout the content. There are a bunch of default options provided for you, but feel free to override any of these. The _Configuration_ section below has more information on what you can change. -It also uses [html-pipeline](https://github.com/jch/html-pipeline) to perform the rendering by default. You can override this by providing a custom rendering class.You must implement two methods: +It uses [Commonmarker](https://github.com/gjtorikian/commonmarker) (v2.x) to perform the Markdown rendering by default, with GitHub Flavored Markdown extensions enabled including automatic header anchors. Emoji shortcodes (like `:smile:`) are automatically converted to emoji characters using [gemoji](https://github.com/github/gemoji). You can override this by providing a custom rendering class. You must implement two methods: - `initialize` - Takes two arguments, the parsed `schema` and the configuration `options`. - `render` Takes the contents of a template page. It also takes two optional kwargs, the GraphQL `type` and its `name`. For example: @@ -404,7 +404,7 @@ The following options are available: | `use_default_styles` | Indicates if you want to use the default styles. | `true` | | `base_url` | Indicates the base URL to prepend for assets and links. | `""` | | `delete_output` | Deletes `output_dir` before generating content. | `false` | -| `pipeline_config` | Defines two sub-keys, `pipeline` and `context`, which are used by `html-pipeline` when rendering your output. | `pipeline` has `ExtendedMarkdownFilter`, `EmojiFilter`, and `TableOfContentsFilter`. `context` has `gfm: false` and `asset_root` set to GitHub's CDN. | +| `pipeline_config` | Defines two sub-keys, `pipeline` and `context`. The `context` hash can contain an `unsafe` key to enable raw HTML rendering (needed for custom layouts). Note: In v6.0+, markdown and emoji rendering are handled directly by the gem, not via html-pipeline filters. | `pipeline` is empty. `context` has `gfm: false`, `unsafe: true`, and `asset_root` set to GitHub's CDN. | | `renderer` | The rendering class to use. | `GraphQLDocs::Renderer` | | `templates` | The templates to use when generating HTML. You may override any of the following keys: `default`, `includes`, `operations`, `objects`, `mutations`, `interfaces`, `enums`, `unions`, `input_objects`, `scalars`, `directives`. | The defaults are found in _lib/graphql-docs/layouts/_. | | `landing_pages` | The landing page to use when generating HTML for each type. You may override any of the following keys: `index`, `query`, `object`, `mutation`, `interface`, `enum`, `union`, `input_object`, `scalar`, `directive`. | The defaults are found in _lib/graphql-docs/landing_pages/_. | diff --git a/graphql-docs.gemspec b/graphql-docs.gemspec index 14878e8..2682831 100644 --- a/graphql-docs.gemspec +++ b/graphql-docs.gemspec @@ -43,11 +43,10 @@ Gem::Specification.new do |spec| spec.add_dependency "graphql", "~> 2.0" # rendering - spec.add_dependency "commonmarker", ">= 0.23.6", "~> 0.23" + spec.add_dependency "commonmarker", "~> 2.0" spec.add_dependency "escape_utils", "~> 1.2" - spec.add_dependency "extended-markdown-filter", "~> 0.4" - spec.add_dependency "gemoji", "~> 3.0" - spec.add_dependency "html-pipeline", ">= 2.14.3", "~> 2.14" + spec.add_dependency "gemoji", "~> 4.0" + spec.add_dependency "html-pipeline", "~> 3.0" spec.add_dependency "sass-embedded", "~> 1.58" spec.add_dependency "ostruct", "~> 0.6" spec.add_dependency "logger", "~> 1.6" diff --git a/lib/graphql-docs/configuration.rb b/lib/graphql-docs/configuration.rb index 99bd1b0..5d193f9 100644 --- a/lib/graphql-docs/configuration.rb +++ b/lib/graphql-docs/configuration.rb @@ -31,10 +31,7 @@ module Configuration delete_output: false, output_dir: "./output/", pipeline_config: { - pipeline: - %i[ExtendedMarkdownFilter - EmojiFilter - TableOfContentsFilter], + pipeline: [], # html-pipeline 3 filters are instantiated differently context: { gfm: false, unsafe: true, # necessary for layout needs, given that it's all HTML templates diff --git a/lib/graphql-docs/helpers.rb b/lib/graphql-docs/helpers.rb index 23cd66b..67ee5ad 100644 --- a/lib/graphql-docs/helpers.rb +++ b/lib/graphql-docs/helpers.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "commonmarker" +require "gemoji" require "ostruct" module GraphQLDocs @@ -64,8 +65,31 @@ def include(filename, opts = {}) def markdownify(string) return "" if string.nil? - type = @options[:pipeline_config][:context][:unsafe] ? :UNSAFE : :DEFAULT - ::CommonMarker.render_html(string, type).strip + begin + # Replace emoji shortcodes before markdown processing + string_with_emoji = emojify(string) + + doc = ::Commonmarker.parse(string_with_emoji) + html = if @options[:pipeline_config][:context][:unsafe] + doc.to_html(options: {render: {unsafe: true}}) + else + doc.to_html + end + html.strip + rescue => e + # Log error and return safe fallback + warn "Failed to parse markdown: #{e.message}" + require "cgi" unless defined?(CGI) + CGI.escapeHTML(string) + end + end + + # Converts emoji shortcodes like :smile: to emoji characters + def emojify(string) + string.gsub(/:([a-z0-9_+-]+):/) do |match| + emoji = Emoji.find_by_alias(Regexp.last_match(1)) + emoji ? emoji.raw : match + end end # Returns the root types (query, mutation) from the parsed schema. diff --git a/lib/graphql-docs/layouts/default.html b/lib/graphql-docs/layouts/default.html index b6a4fb4..877783f 100644 --- a/lib/graphql-docs/layouts/default.html +++ b/lib/graphql-docs/layouts/default.html @@ -167,7 +167,7 @@
diff --git a/lib/graphql-docs/layouts/includes/sidebar.html b/lib/graphql-docs/layouts/includes/sidebar.html index 0f480f5..12d31d7 100644 --- a/lib/graphql-docs/layouts/includes/sidebar.html +++ b/lib/graphql-docs/layouts/includes/sidebar.html @@ -8,7 +8,7 @@ but preserves whitespace structure
+ # Check that the nested structure is maintained
+ assert_match(/nest2/, contents, "Expected 'nest2' to be present in output")
+ assert_match(/nest3/, contents, "Expected 'nest3' to be present in output")
+
+ # Ensure it's in a code block (commonmarker wraps code in pre/code tags)
+ assert_match(/]*>.*nest2.*<\/code>/m, contents, "Expected nest2 to be in a code block")
+
+ # Verify nesting order is preserved (nest3 should appear after nest2)
+ nest2_pos = contents.index("nest2")
+ nest3_pos = contents.index("nest3")
+ assert nest2_pos < nest3_pos, "Expected nest2 to appear before nest3 to maintain nesting structure"
end
def test_that_yaml_frontmatter_title_renders_in_html
diff --git a/test/graphql-docs/renderer_test.rb b/test/graphql-docs/renderer_test.rb
index fd02e45..44c728b 100644
--- a/test/graphql-docs/renderer_test.rb
+++ b/test/graphql-docs/renderer_test.rb
@@ -47,10 +47,7 @@ def test_that_unsafe_html_is_not_blocked_by_default
def test_that_unsafe_html_is_blocked_when_asked
renderer = GraphQLDocs::Renderer.new(@parsed_schema, GraphQLDocs::Configuration::GRAPHQLDOCS_DEFAULTS.merge({
pipeline_config: {
- pipeline:
- %i[ExtendedMarkdownFilter
- EmojiFilter
- TableOfContentsFilter],
+ pipeline: [],
context: {
gfm: false,
unsafe: false,
@@ -63,35 +60,136 @@ def test_that_unsafe_html_is_blocked_when_asked
assert_equal "Oh hello
", contents
end
- def test_that_filename_accessible_to_filters
- renderer = GraphQLDocs::Renderer.new(@parsed_schema, GraphQLDocs::Configuration::GRAPHQLDOCS_DEFAULTS.merge({
- pipeline_config: {
- pipeline:
- %i[ExtendedMarkdownFilter
- EmojiFilter
- TableOfContentsFilter
- AddFilenameFilter],
- context: {
- gfm: false,
- unsafe: true,
- asset_root: "https://a248.e.akamai.net/assets.github.com/images/icons"
- }
- }
- }))
- contents = renderer.render('', type: "Droid", name: "R2D2", filename: "/this/is/the/filename")
- assert_match %r{/this/is/the/filename}, contents
+ # Note: Custom filters are no longer supported in the same way with html-pipeline 3
+ # and commonmarker 2.x. The rendering is now handled directly by commonmarker.
+ # If custom post-processing is needed, it should be done via the Renderer subclass.
+
+ # Tests for commonmarker 2.x GitHub Flavored Markdown features
+
+ def test_that_tables_render_correctly
+ markdown = "| Foo | Bar |\n|-----|-----|\n| 1 | 2 |"
+ html = @renderer.to_html(markdown)
+
+ assert_match(//, html)
+ assert_match(/Foo<\/th>/, html)
+ assert_match(/ Bar<\/th>/, html)
+ assert_match(/ 1<\/td>/, html)
+ assert_match(/ 2<\/td>/, html)
end
-end
-class AddFilenameFilter < HTML::Pipeline::Filter
- def call
- doc.search('span[@id="fill-me"]').each do |span|
- span.inner_html = context[:filename]
- end
- doc
+ def test_that_strikethrough_renders
+ markdown = "~~deleted text~~"
+ html = @renderer.to_html(markdown)
+
+ assert_match(/deleted text<\/del>/, html)
+ end
+
+ def test_that_autolinks_work
+ markdown = "Visit https://example.com for more"
+ html = @renderer.to_html(markdown)
+
+ assert_match(%r{https://example.com}, html)
+ end
+
+ def test_that_task_lists_render
+ markdown = "- [ ] Todo item\n- [x] Done item"
+ html = @renderer.to_html(markdown)
+
+ assert_match(//, html)
+ end
+
+ def test_that_code_blocks_preserve_content
+ markdown = "```json\n{\n \"nested\": {\n \"value\": true\n }\n}\n```"
+ html = @renderer.to_html(markdown)
+
+ assert_match(/nested/, html)
+ assert_match(/value/, html)
+ assert_match(/code<\/code>/, html)
+ end
+
+ def test_that_blockquotes_render
+ markdown = "> This is a quote"
+ html = @renderer.to_html(markdown)
+
+ assert_match(//, html)
+ assert_match(/This is a quote/, html)
+ end
+
+ # Tests for emoji rendering
+
+ def test_that_emoji_shortcodes_are_converted
+ markdown = "Hello :smile: world :heart:"
+ html = @renderer.to_html(markdown)
+
+ assert_match(/😄/, html, "Expected :smile: to be converted to 😄")
+ assert_match(/❤️/, html, "Expected :heart: to be converted to ❤️")
+ refute_match(/:smile:/, html, "Emoji shortcode should be replaced")
+ refute_match(/:heart:/, html, "Emoji shortcode should be replaced")
+ end
+
+ def test_that_unknown_emoji_shortcodes_are_left_unchanged
+ markdown = "Hello :not_a_real_emoji: world"
+ html = @renderer.to_html(markdown)
+
+ assert_match(/:not_a_real_emoji:/, html, "Unknown emoji shortcodes should remain unchanged")
+ end
+
+ def test_that_emoji_works_with_markdown
+ markdown = "**Bold :smile:** and *italic :heart:*"
+ html = @renderer.to_html(markdown)
+
+ assert_match(/Bold 😄<\/strong>/, html)
+ assert_match(/italic ❤️<\/em>/, html)
end
- def validate
- needs :filename
+ def test_that_emoji_in_code_blocks_are_converted
+ markdown = "```\n:smile:\n```"
+ html = @renderer.to_html(markdown)
+
+ # Note: Emoji replacement happens BEFORE markdown parsing, so emoji in code blocks
+ # ARE converted. This is a known limitation of the current implementation.
+ # To preserve emoji shortcodes in code blocks, escape them or use a different approach.
+ assert_match(/😄/, html, "Current implementation converts emoji even in code blocks")
+ end
+
+ # Tests for error handling
+
+ def test_that_malformed_markdown_is_handled_gracefully
+ # Even though commonmarker is forgiving, test the error handling
+ contents = @renderer.to_html(nil)
+ assert_equal "", contents
+ end
+
+ def test_that_empty_string_handled
+ contents = @renderer.to_html("")
+ assert_equal "", contents
+ end
+
+ # Test helpers module emoji support
+
+ def test_that_markdownify_helper_converts_emoji
+ @renderer.extend(GraphQLDocs::Helpers)
+ @renderer.instance_variable_set(:@options, GraphQLDocs::Configuration::GRAPHQLDOCS_DEFAULTS)
+
+ html = @renderer.markdownify("Testing :tada: emoji")
+
+ assert_match(/🎉/, html, "Expected :tada: to be converted to 🎉")
end
end