{"id":29404551,"url":"https://github.com/tenjininc/ghostwriter","last_synced_at":"2025-10-25T01:20:27.005Z","repository":{"id":56874144,"uuid":"48144568","full_name":"TenjinInc/ghostwriter","owner":"TenjinInc","description":"A Ruby gem that rewrites HTML as plain text while preserving as much legibility and functionality as possible.","archived":false,"fork":false,"pushed_at":"2025-01-08T19:55:47.000Z","size":90,"stargazers_count":2,"open_issues_count":2,"forks_count":0,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-07-09T13:29:46.279Z","etag":null,"topics":["email","html","plaintext","ruby"],"latest_commit_sha":null,"homepage":"https://github.com/TenjinInc/ghostwriter","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/TenjinInc.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2015-12-17T01:18:14.000Z","updated_at":"2025-01-08T19:55:46.000Z","dependencies_parsed_at":"2025-01-08T20:48:41.550Z","dependency_job_id":"74f31416-637c-469e-b3e6-2cb81b553d31","html_url":"https://github.com/TenjinInc/ghostwriter","commit_stats":{"total_commits":53,"total_committers":1,"mean_commits":53.0,"dds":0.0,"last_synced_commit":"7ca5ad8c64d219fb247f17dfa2c5cf173ddc3bdc"},"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/TenjinInc/ghostwriter","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TenjinInc%2Fghostwriter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TenjinInc%2Fghostwriter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TenjinInc%2Fghostwriter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TenjinInc%2Fghostwriter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TenjinInc","download_url":"https://codeload.github.com/TenjinInc/ghostwriter/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TenjinInc%2Fghostwriter/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264648521,"owners_count":23643669,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["email","html","plaintext","ruby"],"created_at":"2025-07-10T20:13:09.621Z","updated_at":"2025-10-17T21:08:49.402Z","avatar_url":"https://github.com/TenjinInc.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Ghostwriter\n\nA ruby gem that converts HTML to plain text, preserving as much legibility and functionality as possible.\n\nIt's sort of like a reverse-markdown or a *very* simple screen reader.\n\n## But Why, Though?\n\n* Some email clients won't or can’t offer HTML support.\n* Some people explicitly choose plaintext for accessibility or just plain preference.\n* Spam filters tend to prefer emails with a plain text alternative (but if you use this gem to spam people, \n  not only might you be \n  [breaking](https://fightspam.gc.ca)\n  [various](https://gdpr.eu/)\n  [laws](https://www.ftc.gov/tips-advice/business-center/guidance/can-spam-act-compliance-guide-business), \n  I will also personally curse you)\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'ghostwriter'\n```\n\nAnd then execute:\n\n    bundle\n\nOr install it manually with:\n\n    gem install ghostwriter\n\n## Usage\n\nCreate a `Ghostwriter::Writer` and call `#textify` with the html string you want modified:\n\n```ruby\nhtml = \u003c\u003c~HTML\n    \u003chtml\u003e\n    \u003cbody\u003e\n        \u003cp\u003eThis is some text with \u003ca href=\"tenjin.ca\"\u003ea link\u003c/a\u003e\u003c/p\u003e\n        \u003cp\u003eIt handles other stuff, too.\u003c/p\u003e\n        \u003chr\u003e\n        \u003ch1\u003eStuff Like\u003c/h1\u003e\n        \u003cul\u003e\n          \u003cli\u003eImages\u003c/li\u003e\n          \u003cli\u003eLists\u003c/li\u003e\n          \u003cli\u003eTables\u003c/li\u003e\n          \u003cli\u003eAnd more\u003c/li\u003e\n        \u003c/ul\u003e\n    \u003c/body\u003e\n    \u003c/html\u003e\nHTML\n\nghostwriter = Ghostwriter::Writer.new\n\nputs ghostwriter.textify(html)\n```\n\nProduces:\n\n```\nThis is some text with a link (tenjin.ca)\n\nIt handles other stuff, too.\n\n\n----------\n\n-- Stuff Like --\n- Images\n- Lists\n- Tables\n- And more\n```\n\n### Links\n\nLinks are converted to the link text followed by the link target in brackets:\n\n```html\nVisit our \u003ca href=\"https://example.com\"\u003eWebsite\u003c/a\u003e\n```\n\nBecomes:\n\n```\nVisit our Website (https://example.com)\n```\n\n#### Relative Links\n\nSince emails are wholly distinct from your web address, relative links might break.\n\nTo avoid this problem, either use the `\u003cbase\u003e` header tag:\n\n```html\n\n\u003chtml\u003e\n\u003chead\u003e\n   \u003cbase href=\"https://www.example.com\"\u003e\n\u003c/head\u003e\n\u003cbody\u003e\nUse the base tag to \u003ca href=\"/contact\"\u003eexpand\u003c/a\u003e links.\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\nBecomes:\n\n```\nUse the base tag to expand (https://www.example.com/contact) links.\n```\n\nOr you can use the `link_base` configuration:\n\n```ruby\nGhostwriter::Writer.new(link_base: 'tenjin.ca').textify(html)\n```\n\n### Images\n\nImages with alt text are converted:\n\n```html\n\u003cimg src=\"logo.jpg\" alt=\"ACME Anvils\" /\u003e\n```\n\nBecomes:\n\n```\nACME Anvils (logo.jpg)\n```\n\nBut images lacking alt text or with a presentation ARIA role are ignored:\n\n```html\n\u003c!-- these will just become an empty string --\u003e\n\u003cimg src=\"decoration.jpg\"\u003e\n\u003cimg src=\"logo.jpg\" role=\"presentation\"\u003e\n```\n\nAnd images with data URIs won't include the data portion.\n\n```html\n\n\u003cimg src=\"data:image/gif;base64,R0lGODdhIwAjAMZ/AAkMBxETEBUUDBoaExkaGCIcFx4fGCEfFCcfECkjHiUlHiglGikmFjAqFi8pJCsrJT8sCjMzLDUzJzs0GjkzLTszKTM1Mzg4MD48Mzs+O0tAIElCJ1NCGVdBHUtEMkNFQjlHTFJDOkdGPT1ISUxLRENOT1tMI01PTGdLKk1RU0hTVEtTT0NVVFRTTExYWE9YVGhVP1VZXGFYTWhaMFRcWHFYL1FdXV1dRHdZMVRgYFhgXFdiY11hY1tkX31hJltmZ2pnWnloLGFrbG9oYXlqN3NqTnBqWHxqRItvRIh0Nod0ToF2U5J4LX55Xm97e4B5aZqAQpGAdqOCOZKEYZ2FOJyEVoyKbqiOXpySbLCVcLCXaKWbdKCdfZyhi66dksGdc76fbbije7mkdLOmgq6ogrCpibyvirexisWvhs2vgsGyiLq1lce1lMC5ks28nsfBmcHDq9bAl9PDmMnFo9TGh8zIoM7Jm9vLs9nRo93QqtfSquLQpdXUs+fdterlw////ywAAAAAIwAjAAAH/oArOTo6PYaGOz08P0KMOTZCOzw7PzY/Pz2JPYSDhTSFPTSXPY0tIiIfJz05o5Q/O7A5moc6O4Q0oS8uQisXGCItwTItP5OxOrKjhzSfLzYvgz85ERQXJKcSIkZeJDqOl43StrSEKzo2LhkOGBISDw40JyIVFVEyorBCkZmwtCsrtnLQSJCAwoMFCiwoiECPAr0TjPrtECJwXLMVNARlUCBhQAEFC2SsgWPGDBs3d2RcorSD1SVGr3qskOkihoIH70DO0cOHDx48evD0KQONmQ0aORZJE3VLRYoPBRwoUCCCSx07eoL+xLNnj5UfNFry4BHuR6EcK0qkKJFhAYUE/g+cdHlz1efPrnvM2MjhQlYOWTxktXThIoUKhQoKDHBi5Y0dO0CD5smzJ46NvWJfjYW1w4WKEiWkKkgw9UYdPXTo8Mn6042bvX9pTHoFa5GKzykekP5owEidN1u6PKnzMw+QJ3ttUPr7qKUs0C5KHOyoAMMaNWrmjKlSRYscMFm+nBBUybkLSYsIl3DxwAgcKwWMzGnz5kqTK1e09AEDI0uGE8rJEgNfsuxVggoujGABF1xMoYAVc9RRhxxq5JGVHn3EEYcIGfT1igvGKLfDZyWMkMINa5QhQRNz9CQhT1n5URmHJ8Sygw2BSWLDbaCpgEFPNzxBV4QwApVhHBhg/vABZ0pJIhuCoI0wQhFlkLEGGWfQ9wZ2W6KRBhoUJKncKyK2tMOBPI6wwAxltInlG1uKcQUUV3xpwQUXACSJjbCAxgJoJShggBVtnmGGlm/M4UYcX14QQQQ1PpJjUjmsd5sKCg5gBRdkYMlGG2KwoUYWWYARxgXVnODXqmP9CWgJIESwxhJTbEHGGGbMsSWpaRRBQQQXpPKIiJOgg+BnI4AwwhxcHFHrGGN0KYYYaEhAzQX/7flIDMqx4CoIJY7QxhpY0GorXXXwkUcRj1Lg7gfMDavcCSx4BqsIHpyxRhtT1FCDEmNgF4YY1j6KZ4eXXTast9GVcAIHG2TZRhlT/qCAAg5IZIzCA+1QQ0EGKbgAG7c0pPOAAgQcwEQSZ2R5RhlYVIFEFVccAQEAAASgWEIrXEZYDDHQYAEBAQSAcxBUbCExGWVsMfMVCHSA89QCbHBDX4QRRsPURuMcQBBQYLHGHGuwoYUYVdQQxAIOBCCACVLUgDMBS7rwwgtENHDAAEYLMIAAHhABRRVYKFEDDjjU0AA9HiQhxQQOCDC1BXe/UAQVVATRwAIDDGCAAAd0EAQTTEgBBQ4IIFSBFHFPdYEIFJBAQOUE1K5AAyZgnsQME/jNwAG/e7QBFT4sYEABBiQv6ANDDLDCCwPULr0ADYyeOQcMLMAAAxNAIQUHJwckYEDn5CfvgAEKvECA3+R7nrwB2k+ggQkmaLB3++Sz3zkMIawQCAA7\"\n     alt=\"Data picture\" /\u003e\n```\n\nBecomes:\n\n```\nData picture (embedded)\n```\n\n### Paragraphs and Linebreaks\n\nParagraphs are padded with a newline at the end. Line break tags add an empty line.\n\n```html\n\u003cp\u003eI would like to propose a toast.\u003c/p\u003e\n\u003cp\u003eThis meal we enjoy together would be improved by one.\u003c/p\u003e\n\u003cbr /\u003e\n\u003cp\u003e... Plug in the toaster and I'll get the bread.\u003c/p\u003e\n```\n\n```\nI would like to propose a toast.\n\nThis meal we enjoy together would be improved by one.\n\n\n... Plug in the toaster and I'll get the bread.\n\n```\n\n### Headings\n\nHeadings are wrapped with a marker per heading level:\n\n```html\n\u003ch1\u003eDog Maintenance and Repair\u003c/h1\u003e\n\u003ch2\u003eFood Input Port\u003c/h2\u003e\n\u003ch3\u003eExhaust Port Considerations\u003c/h3\u003e\n```\n\nBecomes:\n\n```\n-- Dog Maintenance and Repair --\n---- Food Input Port ----\n------ Exhaust Port Considerations ------\n```\n\nThe `\u003cheader\u003e` tag is treated like an `\u003ch1\u003e` tag.\n\n### Lists\n\nLists are converted, too. They are padded with newlines and are given simple markers:\n\n```html\n\n\u003cul\u003e\n   \u003cli\u003ePlanes\u003c/li\u003e\n   \u003cli\u003eTrains\u003c/li\u003e\n   \u003cli\u003eAutomobiles\u003c/li\u003e\n\u003c/ul\u003e\n\u003col\u003e\n   \u003cli\u003eI get knocked down\u003c/li\u003e\n   \u003cli\u003eI get up again\u003c/li\u003e\n   \u003cli\u003eNever gonna keep me down\u003c/li\u003e\n\u003c/ol\u003e\n```\n\nBecomes:\n\n```\n- Planes\n- Trains\n- Automobiles\n\n1. I get knocked down\n2. I get up again\n3. Never gonna keep me down\n```\n\n### Tables\n\nTables are still often used in email structuring because support for more modern HTML and CSS is inconsistent. If your\ntable is purely presentational, mark it with `role=\"presentation\"`. See below for details.\n\nFor real data tables, Ghostwriter tries to maintain table structure for simple tables:\n\n```html\n\n\u003ctable\u003e\n   \u003cthead\u003e\n   \u003ctr\u003e\n      \u003cth\u003eShip\u003c/th\u003e\n      \u003cth\u003eCaptain\u003c/th\u003e\n   \u003c/tr\u003e\n   \u003c/thead\u003e\n   \u003ctbody\u003e\n   \u003ctr\u003e\n      \u003ctd\u003eEnterprise\u003c/td\u003e\n      \u003ctd\u003eJean-Luc Picard\u003c/td\u003e\n   \u003c/tr\u003e\n   \u003ctr\u003e\n      \u003ctd\u003eTARDIS\u003c/td\u003e\n      \u003ctd\u003eThe Doctor\u003c/td\u003e\n   \u003c/tr\u003e\n   \u003ctr\u003e\n      \u003ctd\u003ePlanet Express Ship\u003c/td\u003e\n      \u003ctd\u003eTuranga Leela\u003c/td\u003e\n   \u003c/tr\u003e\n   \u003c/tbody\u003e\n\u003c/table\u003e\n```\n\nBecomes:\n\n```\n| Ship                | Captain         |\n|---------------------|-----------------|\n| Enterprise          | Jean-Luc Picard |\n| TARDIS              | The Doctor      |\n| Planet Express Ship | Turanga Leela   |\n```\n\n### Customizing Output\n\nGhostwriter has some constructor options to customize output.\n\nYou can set heading markers.\n\n```ruby\nhtml = \u003c\u003c~HTML\n   \u003ch1\u003eEmergency Cat Procedures\u003c/h1\u003e\nHTML\n\nwriter = Ghostwriter::Writer.new(heading_marker: '#')\n\nputs writer.textify(html)\n```\n\nProduces:\n\n```\n# Emergency Cat Procedures #\n```\n\nYou can also set list item markers. Ordered markers can be anything that responds to `#next` (eg. any `Enumerator`)\n\n```ruby\nhtml = \u003c\u003c~HTML\n   \u003col\u003e\u003cli\u003eMercury\u003c/li\u003e\u003cli\u003eVenus\u003c/li\u003e\u003cli\u003eMars\u003c/li\u003e\u003c/ol\u003e\n   \u003cul\u003e\u003cli\u003eTeapot\u003c/li\u003e\u003cli\u003eKettle\u003c/li\u003e\u003c/ul\u003e\nHTML\n\nwriter = Ghostwriter::Writer.new(ul_marker: '*', ol_marker: 'a')\n\nputs writer.textify(html)\n```\n\nProduces:\n\n```\na. Mercury\nb. Venus\nc. Mars\n\n* Teapot\n* Kettle\n```\n\nAnd tables can be customized:\n\n```ruby\nwriter = Ghostwriter::Writer.new(table_row:    '.',\n                                 table_column: '#',\n                                 table_corner: '+')\n\nputs writer.textify \u003c\u003c~HTML\n   \u003ctable\u003e\n      \u003cthead\u003e\n         \u003ctr\u003e\u003cth\u003eMoon\u003c/th\u003e\u003cth\u003ePortfolio\u003c/th\u003e\u003c/tr\u003e\n      \u003c/thead\u003e\n      \u003ctbody\u003e\n         \u003ctr\u003e\u003ctd\u003ePhobos\u003c/td\u003e\u003ctd\u003eFear \u0026 Panic\u003c/td\u003e\u003c/tr\u003e\n         \u003ctr\u003e\u003ctd\u003eDeimos\u003c/td\u003e\u003ctd\u003eDread and Terror\u003c/td\u003e\u003c/tr\u003e\n      \u003c/tbody\u003e\n   \u003c/table\u003e\nHTML\n```\n\nProduces:\n\n```\n# Moon   # Portfolio        #\n+........+..................+\n# Phobos # Fear \u0026 Panic     #\n# Deimos # Dread and Terror #\n\n```\n\n#### Presentation ARIA Role\n\nTags with `role=\"presentation\"` will be treated as a simple container and the normal behaviour will be suppressed.\n\n```html\n\n\u003ctable role=\"presentation\"\u003e\n   \u003ctr\u003e\n      \u003ctd\u003eThe table is a lie\u003c/td\u003e\n   \u003c/tr\u003e\n\u003c/table\u003e\n\u003cul role=\"presentation\"\u003e\n   \u003cli\u003eNo such list\u003c/li\u003e\n\u003c/ul\u003e\n```\n\nBecomes:\n\n```\nThe table is a lie\nNo such list\n```\n\n### Mail Gem Example\n\nTo use `#textify` with the [mail](https://github.com/mikel/mail) gem, just provide the text-part by pasisng the html\nthrough Ghostwriter:\n\n```ruby\nrequire 'mail'\n\nhtml        = 'My email and a \u003ca href=\"https://tenjin.ca\"\u003elink\u003c/a\u003e'\nghostwriter = Ghostwriter::Writer.new\n\nMail.deliver do\n   to 'bob@example.com'\n   from 'dot@example.com'\n   subject 'Using Ghostwriter with Mail'\n\n   html_part do\n      content_type 'text/html; charset=UTF-8'\n      body html\n   end\n\n   text_part do\n      body ghostwriter.textify(html)\n   end\nend\n\n```\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/TenjinInc/ghostwriter\n\nThis project is intended to be a friendly space for collaboration, and contributors are expected to adhere to the\n[Contributor Covenant](contributor-covenant.org) code of conduct.\n\n### Core Developers\n\nAfter checking out the repo, run `bundle install` to install dependencies. Then, run `rake spec` to run the tests. You\ncan also run `bin/console` for an interactive prompt that will allow you to experiment.\n\n#### Local Install\n\nTo install this gem onto your local machine only, run\n\n`bundle exec rake install`\n\n#### Gem Release\n\nTo release a gem to the world at large\n\n1. Update the version number in `version.rb`,\n2. Run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push\n   the `.gem` file to [rubygems.org](https://rubygems.org).\n3. Do a wee dance\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftenjininc%2Fghostwriter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftenjininc%2Fghostwriter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftenjininc%2Fghostwriter/lists"}