commit 50c9c20ecad8f0068d9d633253dc4030f70fb3b5 Author: mathias234 Date: Tue Dec 2 10:23:52 2025 +0100 Initial project setup diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3aeeb71 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +*.gem +*.rbc +.bundle +.config +coverage +pkg +spec/reports +tmp +.rspec_status +Gemfile.lock +*.docx +*.docx# diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..277147a --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,22 @@ +AllCops: + TargetRubyVersion: 3.0 + NewCops: enable + SuggestExtensions: false + +Style/StringLiterals: + EnforcedStyle: double_quotes + +Style/StringLiteralsInInterpolation: + EnforcedStyle: double_quotes + +Style/Documentation: + Enabled: false + +Layout/LineLength: + Max: 120 + +Metrics: + Enabled: false + +Gemspec/DevelopmentDependencies: + Enabled: false diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..be173b2 --- /dev/null +++ b/Gemfile @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gemspec diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..398c486 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Mathias + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bfba3ab --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# Ezdoc + +A Ruby gem for working with docx files. + +## Installation + +Add this line to your application's Gemfile: + +```ruby +gem 'ezdoc' +``` + +And then execute: + + $ bundle install + +Or install it yourself as: + + $ gem install ezdoc + +## Usage + +```ruby +require 'ezdoc' +``` + +## Development + +After checking out the repo, run `bundle install` to install dependencies. Then, run `rake spec` to run the tests. + +## License + +The gem is available as open source under the terms of the [MIT License](LICENSE.txt). diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..e27f4e9 --- /dev/null +++ b/Rakefile @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "bundler/gem_tasks" +require "rake/testtask" +require "rubocop/rake_task" + +Rake::TestTask.new(:test) do |t| + t.libs << "test" + t.libs << "lib" + t.test_files = FileList["test/**/*_test.rb"] +end + +RuboCop::RakeTask.new + +task default: %i[test rubocop] diff --git a/ezdoc.gemspec b/ezdoc.gemspec new file mode 100644 index 0000000..ed2dd56 --- /dev/null +++ b/ezdoc.gemspec @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require_relative "lib/ezdoc/version" + +Gem::Specification.new do |spec| + spec.name = "ezdoc" + spec.version = Ezdoc::VERSION + spec.authors = ["Mathias"] + spec.summary = "A Ruby gem for working with docx files" + spec.description = "Easy document manipulation for docx files in Ruby" + spec.homepage = "https://github.com/mathias/ezdoc" + spec.license = "MIT" + spec.required_ruby_version = ">= 3.0.0" + + spec.files = Dir.glob("{lib}/**/*") + %w[README.md LICENSE.txt] + spec.require_paths = ["lib"] + + spec.add_dependency "nokogiri", "~> 1.18" + spec.add_dependency "rubyzip", "~> 2.3" + + spec.add_development_dependency "bundler", "~> 2.0" + spec.add_development_dependency "minitest", "~> 5.0" + spec.add_development_dependency "rake", "~> 13.0" + spec.add_development_dependency "rubocop", "~> 1.69" + spec.metadata["rubygems_mfa_required"] = "true" +end diff --git a/lib/ezdoc.rb b/lib/ezdoc.rb new file mode 100644 index 0000000..bbb5882 --- /dev/null +++ b/lib/ezdoc.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require "nokogiri" + +require_relative "ezdoc/version" +require_relative "ezdoc/xml/content_types" +require_relative "ezdoc/xml/relationships" +require_relative "ezdoc/xml/document_xml" +require_relative "ezdoc/package" +require_relative "ezdoc/document" + +module Ezdoc + class Error < StandardError; end +end diff --git a/lib/ezdoc/document.rb b/lib/ezdoc/document.rb new file mode 100644 index 0000000..dff2b0d --- /dev/null +++ b/lib/ezdoc/document.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Ezdoc + class Document + attr_reader :content + + def self.create(path, &block) + doc = new + block.call(doc) + doc.save(path) + doc + end + + def initialize + @content = [] + end + + def text(value) + @content << { text: value } + end + + def save(path) + Package.new(self).save(path) + end + end +end diff --git a/lib/ezdoc/package.rb b/lib/ezdoc/package.rb new file mode 100644 index 0000000..c2896f0 --- /dev/null +++ b/lib/ezdoc/package.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require "zip" + +module Ezdoc + class Package + def initialize(document) + @document = document + end + + def save(path) + Zip::File.open(path, Zip::File::CREATE) do |zipfile| + zipfile.get_output_stream("[Content_Types].xml") { |f| f.write(content_types_xml) } + zipfile.get_output_stream("_rels/.rels") { |f| f.write(relationships_xml) } + zipfile.get_output_stream("word/_rels/document.xml.rels") { |f| f.write(document_relationships_xml) } + zipfile.get_output_stream("word/document.xml") { |f| f.write(document_xml) } + end + end + + private + + def content_types_xml + Xml::ContentTypes.new.to_xml + end + + def relationships_xml + Xml::Relationships.new.to_xml + end + + def document_relationships_xml + Xml::DocumentRelationships.new.to_xml + end + + def document_xml + Xml::DocumentXml.new(@document.content).to_xml + end + end +end diff --git a/lib/ezdoc/version.rb b/lib/ezdoc/version.rb new file mode 100644 index 0000000..8ac1e77 --- /dev/null +++ b/lib/ezdoc/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Ezdoc + VERSION = "0.0.1" +end diff --git a/lib/ezdoc/xml/content_types.rb b/lib/ezdoc/xml/content_types.rb new file mode 100644 index 0000000..0eb3872 --- /dev/null +++ b/lib/ezdoc/xml/content_types.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Ezdoc + module Xml + class ContentTypes + NAMESPACE = "http://schemas.openxmlformats.org/package/2006/content-types" + + def to_xml + builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml| + xml.Types(xmlns: NAMESPACE) do + xml.Default(Extension: "rels", ContentType: "application/vnd.openxmlformats-package.relationships+xml") + xml.Default(Extension: "xml", ContentType: "application/xml") + xml.Override( + PartName: "/word/document.xml", + ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml" + ) + end + end + builder.to_xml + end + end + end +end diff --git a/lib/ezdoc/xml/document_xml.rb b/lib/ezdoc/xml/document_xml.rb new file mode 100644 index 0000000..99509d6 --- /dev/null +++ b/lib/ezdoc/xml/document_xml.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Ezdoc + module Xml + class DocumentXml + NAMESPACES = { + "xmlns:w" => "http://schemas.openxmlformats.org/wordprocessingml/2006/main", + "xmlns:r" => "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + }.freeze + + def initialize(content) + @content = content + end + + def to_xml + builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml| + xml.document(NAMESPACES) do + xml.parent.namespace = xml.parent.namespace_definitions.find { |ns| ns.prefix == "w" } + xml["w"].body do + @content.each do |item| + xml["w"].p do + xml["w"].r do + xml["w"].t item[:text] + end + end + end + end + end + end + builder.to_xml + end + end + end +end diff --git a/lib/ezdoc/xml/relationships.rb b/lib/ezdoc/xml/relationships.rb new file mode 100644 index 0000000..5221928 --- /dev/null +++ b/lib/ezdoc/xml/relationships.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Ezdoc + module Xml + class Relationships + NAMESPACE = "http://schemas.openxmlformats.org/package/2006/relationships" + + def to_xml + builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml| + xml.Relationships(xmlns: NAMESPACE) do + xml.Relationship( + Id: "rId1", + Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument", + Target: "word/document.xml" + ) + end + end + builder.to_xml + end + end + + class DocumentRelationships + NAMESPACE = "http://schemas.openxmlformats.org/package/2006/relationships" + + def to_xml + builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml| + xml.Relationships(xmlns: NAMESPACE) + end + builder.to_xml + end + end + end +end diff --git a/test/ezdoc_test.rb b/test/ezdoc_test.rb new file mode 100644 index 0000000..99a0cc9 --- /dev/null +++ b/test/ezdoc_test.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require "test_helper" +require "tempfile" +require "zip" + +class EzdocTest < Minitest::Test + def test_creates_valid_docx_with_hello_world + Tempfile.create(["test", ".docx"]) do |file| + Ezdoc::Document.create(file.path) do |doc| + doc.text "Hello World" + end + + assert File.exist?(file.path) + assert File.size(file.path).positive? + + Zip::File.open(file.path) do |zip| + assert zip.find_entry("[Content_Types].xml") + assert zip.find_entry("_rels/.rels") + assert zip.find_entry("word/document.xml") + assert zip.find_entry("word/_rels/document.xml.rels") + + document_xml = zip.read("word/document.xml") + assert_includes document_xml, "Hello World" + end + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..806ca9d --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +$LOAD_PATH.unshift File.expand_path("../lib", __dir__) +require "ezdoc" +require "minitest/autorun"