├── .gitignore ├── .rspec ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── bin ├── console ├── setup └── test ├── exe └── zettel_outline ├── lib ├── zettel_outline.rb └── zettel_outline │ ├── archive.rb │ ├── outline.rb │ ├── outline_item.rb │ ├── outline_parser.rb │ ├── version.rb │ ├── zettel.rb │ ├── zettel_info.rb │ └── zettel_renderer.rb ├── spec ├── spec_helper.rb └── zettel_outline │ ├── archive_spec.rb │ ├── outline_item_spec.rb │ ├── outline_parser_spec.rb │ ├── outline_spec.rb │ ├── zettel_info_spec.rb │ ├── zettel_renderer_spec.rb │ └── zettel_spec.rb └── zettel_outline.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | ## Specific to RubyMotion: 14 | .dat* 15 | .repl_history 16 | build/ 17 | 18 | ## Documentation cache and generated files: 19 | /.yardoc/ 20 | /_yardoc/ 21 | /doc/ 22 | /rdoc/ 23 | 24 | ## Environment normalization: 25 | /.bundle/ 26 | /vendor/bundle 27 | /lib/bundler/man/ 28 | 29 | # for a library or gem, you might want to ignore these files since the code is 30 | # intended to run in multiple environments; otherwise, check them in: 31 | # Gemfile.lock 32 | .ruby-version 33 | .ruby-gemset 34 | 35 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 36 | .rvmrc 37 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This code of conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at christian.tietze@gmail.com. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 45 | version 1.3.0, available at 46 | [http://contributor-covenant.org/version/1/3/0/][version] 47 | 48 | [homepage]: http://contributor-covenant.org 49 | [version]: http://contributor-covenant.org/version/1/3/0/ -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | zettel_outline (0.1.1) 5 | colorize (~> 0.8) 6 | slop (~> 4.9) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | colorize (0.8.1) 12 | diff-lcs (1.4.4) 13 | rake (13.0.3) 14 | rspec (3.10.0) 15 | rspec-core (~> 3.10.0) 16 | rspec-expectations (~> 3.10.0) 17 | rspec-mocks (~> 3.10.0) 18 | rspec-core (3.10.1) 19 | rspec-support (~> 3.10.0) 20 | rspec-expectations (3.10.1) 21 | diff-lcs (>= 1.2.0, < 2.0) 22 | rspec-support (~> 3.10.0) 23 | rspec-mocks (3.10.2) 24 | diff-lcs (>= 1.2.0, < 2.0) 25 | rspec-support (~> 3.10.0) 26 | rspec-support (3.10.2) 27 | slop (4.9.0) 28 | 29 | PLATFORMS 30 | ruby 31 | 32 | DEPENDENCIES 33 | bundler (~> 2.1) 34 | rake (~> 13.0) 35 | rspec (~> 3.10) 36 | zettel_outline! 37 | 38 | BUNDLED WITH 39 | 2.1.4 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Christian Tietze 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZettelOutline Rendering 2 | 3 | Assemble a first draft from your [Zettelkasten](https://zettelkasten.de) notes and an outline file. **Markdown** aware, allowing you to write arbitrarily nested lists as outlines. 4 | 5 | 6 | ## Installation 7 | 8 | Install the ruby gem yourself from the command line: 9 | 10 | $ gem install zettel_outline 11 | 12 | ## Usage of the program 13 | 14 | From the command line, run `zettel_outline` with the required parameters as the help indicates: 15 | 16 | $ zettel_outline 17 | usage: zettel_outline -f -a -o 18 | 19 | Required options: 20 | -f, --file an outline file 21 | -a, --archive path to your Zettel notes 22 | -o, --output file to write results to 23 | 24 | Also available: 25 | -h, --help prints this help 26 | -v, --verbose verbose output 27 | --version 28 | 29 | For example: 30 | 31 | $ zettel_outline -f outline.txt -a /path/to/notes/ -o draft.txt 32 | 33 | This will read the `outline.txt` from the current directory. It will resolve every Zettel reference using the Zettel note archive (`/path/to/notes/`) and concatenate the notes's contents into `draft.txt`. 34 | 35 | A sample outline can look like this: 36 | 37 | * 201407030825 Why baking is so important for life. I really love baking 38 | * 201601231448 Banana cake. The very best cake ever 39 | * 201601222058 Nutritional value of bananas 40 | * 201601222035 Nutritional value of eggs 41 | 42 | The nested list of Zettel references will be split into: 43 | 44 | 1. The Zettel ID, used to find the note in your archive; e.g. "201407030825" 45 | 2. The note title, used for the draft's output; e.g. "Why baking is so important for life" 46 | 3. The comment, which is everything after the first period in the line; e.g. "I really love baking" 47 | 48 | Zettel note contents will be separated using Markdown-enabled HTML comments, which are surrounded with ``. So the resulting `draft.txt` will look similar to this: 49 | 50 | 51 | 52 | 53 | Baking is one of the oldest and definitely one of the 54 | most delicious ways to prepare food. 55 | 56 | ... 57 | 58 | 59 | ## Usage of the library 60 | 61 | The higher-level interface of the gem is very simple. Wrapping every parameter into a `compile` function can look like this: 62 | 63 | ```ruby 64 | def compile(outline_path, notes_path, draft_path) 65 | content = File.read(outline_path) 66 | result = ZettelOutline::compile(content, notes_path) 67 | File.open(draft_path, "w") do |f| 68 | f.write(ZettelOutline::render(result)) 69 | end 70 | end 71 | ``` 72 | 73 | * `ZettelOutline::compile` takes the outline as a string and the path to resolve note references. 74 | * `ZettelOutline::render` is just a wrapper to concatenate the result and return a simple string. 75 | 76 | 77 | ## Contributing 78 | 79 | Bug reports and pull requests are always welcome! [Discuss on the forums.](https://forum.zettelkasten.de) 80 | 81 | 82 | ## License 83 | 84 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 85 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "zettel_outline/zettel_outline" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "zettel_outline" 5 | 6 | PATH = File.expand_path("~/Archiv/") 7 | 8 | testoutline = < :optional 20 | o.on "--version", :argument => :optional do 21 | puts "zettel_outline #{ZettelOutline::VERSION} (#{ZettelOutline::DATE})" 22 | exit 23 | end 24 | end 25 | 26 | if ARGV.include?('-h') || ARGV.include?('--help') 27 | puts opts 28 | exit 29 | end 30 | 31 | class Command 32 | attr_accessor :file, :archive, :output, :verbose 33 | 34 | def initialize(opts) 35 | abort("--file required") if opts[:file].nil? 36 | abort("--archive required") if opts[:archive].nil? 37 | abort("--output required") if opts[:output].nil? 38 | 39 | opts.to_hash.each do |k, v| send("#{k}=", v) end 40 | end 41 | 42 | def check_permissions! 43 | abort("Could not open file '#{@file}'.".red) unless File.readable?(@file) 44 | abort("Could not write to file '#{@output}'.") if File.directory?(@output) 45 | abort("Zettel archive does not exist at '#{@archive}'.".red) unless File.directory?(@archive) 46 | end 47 | 48 | def execute 49 | check_permissions! 50 | 51 | in_path = File.expand_path(@file) 52 | log("Reading in outline from #{in_path} ...") 53 | content = File.read(@file) 54 | 55 | log("Compiling draft from Zettel ...") 56 | result = ZettelOutline::compile(content, @archive) 57 | 58 | out_path = File.expand_path(@output) 59 | log("Writing to #{out_path} ...") 60 | File.open(out_path, "w") do |f| 61 | f.write(ZettelOutline::render(result)) 62 | end 63 | end 64 | 65 | def log(msg) 66 | puts msg.green if @verbose 67 | end 68 | end 69 | 70 | Command.new(opts).execute 71 | -------------------------------------------------------------------------------- /lib/zettel_outline.rb: -------------------------------------------------------------------------------- 1 | require "zettel_outline/version" 2 | require 'zettel_outline/outline' 3 | require 'zettel_outline/outline_item' 4 | require 'zettel_outline/archive' 5 | require 'zettel_outline/zettel_info' 6 | require 'zettel_outline/zettel_renderer' 7 | require 'zettel_outline/zettel' 8 | 9 | module ZettelOutline 10 | 11 | class << self 12 | 13 | def compile(outline_content, folder) 14 | outline = Outline.new(outline_content) 15 | all_items = outline.map_files { |f| ZettelInfo.new(f) } 16 | .map { |info| OutlineItem.new(info) } 17 | renderer = ZettelRenderer.new() 18 | archive = Archive.new(folder) 19 | 20 | all_items.map { |z| z.render(renderer, archive) } 21 | end 22 | 23 | def render(outline) 24 | outline.join("") 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/zettel_outline/archive.rb: -------------------------------------------------------------------------------- 1 | require 'zettel_outline/zettel' 2 | 3 | module ZettelOutline 4 | 5 | class Finder 6 | def file_path(folder, id) 7 | Dir.glob(File.join(folder, "#{id}*")).first 8 | end 9 | end 10 | 11 | class Archive 12 | def initialize(folder) 13 | @folder = folder 14 | end 15 | 16 | def zettel(id, finder = Finder.new) 17 | create_zettel(path(id, finder)) 18 | end 19 | 20 | def create_zettel(path) 21 | return NullZettel.new if path.nil? 22 | 23 | Zettel.new(path) 24 | end 25 | 26 | def path(id, finder = Finder.new) 27 | finder.file_path(@folder, id) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/zettel_outline/outline.rb: -------------------------------------------------------------------------------- 1 | require "zettel_outline/outline_parser" 2 | 3 | module ZettelOutline 4 | class Outline 5 | attr_reader :content 6 | attr_reader :files 7 | 8 | def initialize(content) 9 | @content = content || "" 10 | end 11 | 12 | def files(parser = OutlineParser.new) 13 | return [] if @content.empty? 14 | 15 | @files = parser.parse(content) if @files.nil? 16 | @files 17 | end 18 | 19 | def map_files(parser = OutlineParser.new, &block) 20 | if block_given? 21 | files(parser).map { |f| yield f } 22 | else 23 | to_enum(:map_files, parser) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/zettel_outline/outline_item.rb: -------------------------------------------------------------------------------- 1 | module ZettelOutline 2 | class OutlineItem 3 | 4 | def initialize(zettel_info) 5 | @zettel_info = zettel_info 6 | end 7 | 8 | def render(renderer, archive) 9 | renderer.render(zettel(archive), @zettel_info) 10 | end 11 | 12 | private 13 | 14 | def zettel(archive) 15 | id = @zettel_info.id 16 | archive.zettel(id) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/zettel_outline/outline_parser.rb: -------------------------------------------------------------------------------- 1 | module ZettelOutline 2 | class OutlineParser 3 | def parse(content) 4 | content.each_line 5 | .map { |l| l.strip } 6 | .select { |l| !l.empty? } 7 | .map { |l| cleanup(l) } 8 | end 9 | 10 | private 11 | 12 | def cleanup(line) 13 | raise "empty line" if line.nil? || line.empty? 14 | 15 | line[/^[\* ]*(.+)$/, 1] 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/zettel_outline/version.rb: -------------------------------------------------------------------------------- 1 | module ZettelOutline 2 | VERSION = "0.1.1" 3 | DATE = "2016-01-23" 4 | end 5 | -------------------------------------------------------------------------------- /lib/zettel_outline/zettel.rb: -------------------------------------------------------------------------------- 1 | module ZettelOutline 2 | class Zettel 3 | def initialize(path) 4 | raise "file path needed" if path.nil? 5 | raise "file expected at path" unless File.file?(path) 6 | 7 | @path = path 8 | end 9 | 10 | def render 11 | remove_header(file_contents).join("") 12 | end 13 | 14 | def file_contents 15 | File.readlines(@path) 16 | end 17 | 18 | def remove_header(text) 19 | text 20 | end 21 | end 22 | 23 | class NullZettel 24 | def render 25 | "" 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/zettel_outline/zettel_info.rb: -------------------------------------------------------------------------------- 1 | module ZettelOutline 2 | class ZettelInfo 3 | 4 | def initialize(line) 5 | @line = line 6 | end 7 | 8 | def id 9 | @line[/[0-9]+/] 10 | end 11 | 12 | def title 13 | @line[/[0-9]+ ([\d\w_\- ]+?)(?:\..*)?$/, 1] 14 | end 15 | 16 | def comment 17 | @line[/[0-9]+ [\d\w_\- ]+\.[ ]*(.*)$/, 1] 18 | end 19 | 20 | def info 21 | { id: id, title: title, comment: comment } 22 | end 23 | 24 | def render 25 | "".tap do |output| 26 | output << "\n" 27 | output << %Q{\n} 28 | output << %Q{\n} unless comment.nil? 29 | output << "\n" 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/zettel_outline/zettel_renderer.rb: -------------------------------------------------------------------------------- 1 | module ZettelOutline 2 | class ZettelRenderer 3 | 4 | def render(contents, separator) 5 | "".tap do |output| 6 | output << separator.render 7 | output << contents.render 8 | output << "\n\n" 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'zettel_outline' 3 | 4 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 5 | RSpec.configure do |config| 6 | 7 | config.expect_with :rspec do |expectations| 8 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 9 | end 10 | 11 | config.mock_with :rspec do |mocks| 12 | mocks.verify_partial_doubles = true 13 | end 14 | 15 | # The settings below are suggested to provide a good initial experience 16 | # with RSpec, but feel free to customize to your heart's content. 17 | =begin 18 | # These two settings work together to allow you to limit a spec run 19 | # to individual examples or groups you care about by tagging them with 20 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 21 | # get run. 22 | config.filter_run :focus 23 | config.run_all_when_everything_filtered = true 24 | 25 | # Allows RSpec to persist some state between runs in order to support 26 | # the `--only-failures` and `--next-failure` CLI options. We recommend 27 | # you configure your source control system to ignore this file. 28 | config.example_status_persistence_file_path = "spec/examples.txt" 29 | 30 | # Limits the available syntax to the non-monkey patched syntax that is 31 | # recommended. For more details, see: 32 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 33 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 34 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 35 | config.disable_monkey_patching! 36 | 37 | # This setting enables warnings. It's recommended, but in some cases may 38 | # be too noisy due to issues in dependencies. 39 | config.warnings = true 40 | 41 | # Many RSpec users commonly either run the entire suite or an individual 42 | # file, and it's useful to allow more verbose output when running an 43 | # individual spec file. 44 | if config.files_to_run.one? 45 | # Use the documentation formatter for detailed output, 46 | # unless a formatter has already been configured 47 | # (e.g. via a command-line flag). 48 | config.default_formatter = 'doc' 49 | end 50 | 51 | # Print the 10 slowest examples and example groups at the 52 | # end of the spec run, to help surface which specs are running 53 | # particularly slow. 54 | config.profile_examples = 10 55 | 56 | # Run specs in random order to surface order dependencies. If you find an 57 | # order dependency and want to debug it, you can fix the order by providing 58 | # the seed, which is printed after each run. 59 | # --seed 1234 60 | config.order = :random 61 | 62 | # Seed global randomization in this process using the `--seed` CLI option. 63 | # Setting this allows you to use `--seed` to deterministically reproduce 64 | # test failures related to randomization by passing the same `--seed` value 65 | # as the one that triggered the failure. 66 | Kernel.srand config.seed 67 | =end 68 | end 69 | -------------------------------------------------------------------------------- /spec/zettel_outline/archive_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'zettel_outline/archive' 3 | 4 | class TestableArchive < ZettelOutline::Archive 5 | attr_reader :did_create_zettel_with 6 | 7 | def create_zettel(path) 8 | @did_create_zettel_with = path 9 | end 10 | end 11 | 12 | describe ZettelOutline::Archive do 13 | let(:folder) { "some/folder/path" } 14 | 15 | describe 'fetching a Zettel' do 16 | let(:id) { "the ID" } 17 | 18 | context 'when a file was found' do 19 | subject(:archive) { TestableArchive.new(folder) } 20 | 21 | let(:path) { "the/resulting/path" } 22 | let(:finder) { double(:file_path => path) } 23 | 24 | before(:each) do 25 | archive.zettel(id, finder) 26 | end 27 | 28 | it 'fetches path from finder' do 29 | expect(finder).to have_received(:file_path).with(folder, id) 30 | end 31 | 32 | it 'creates zettel from resulting path' do 33 | expect(archive.did_create_zettel_with).to eq path 34 | end 35 | end 36 | 37 | context 'when no file was found' do 38 | subject(:archive) { described_class.new(folder) } 39 | 40 | let(:finder) { double(:file_path => nil) } 41 | let!(:result) { archive.zettel(id, finder) } 42 | 43 | it 'fetches path from finder' do 44 | expect(finder).to have_received(:file_path).with(folder, id) 45 | end 46 | 47 | it 'returns NullZettel' do 48 | expect(result.class).to equal ZettelOutline::NullZettel 49 | end 50 | end 51 | end 52 | end -------------------------------------------------------------------------------- /spec/zettel_outline/outline_item_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'zettel_outline/outline_item' 3 | 4 | describe ZettelOutline::OutlineItem do 5 | describe 'rendering' do 6 | let(:id) { "123" } 7 | let(:info) { double(:id => id) } 8 | subject(:item) { ZettelOutline::OutlineItem.new(info) } 9 | 10 | let(:rendition) { "the result" } 11 | let(:renderer) { double(:render => rendition) } 12 | 13 | let(:zettel) { double() } 14 | let(:archive) { double(:zettel => zettel) } 15 | 16 | 17 | it 'requests zettel from archive' do 18 | item.render(renderer, archive) 19 | 20 | expect(archive).to have_received(:zettel).with(id) 21 | end 22 | 23 | it 'delegates to renderer' do 24 | item.render(renderer, archive) 25 | 26 | expect(renderer).to have_received(:render).with(zettel, info) 27 | end 28 | 29 | it 'returns renderer result' do 30 | expect(item.render(renderer, archive)).to eq rendition 31 | end 32 | end 33 | end -------------------------------------------------------------------------------- /spec/zettel_outline/outline_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'zettel_outline/outline_parser' 3 | 4 | TEST_OUTLINE = < files) } 12 | 13 | describe 'file list' do 14 | let!(:result) { outline.files(parser) } 15 | 16 | it 'forwards data to parser' do 17 | expect(parser).to have_received(:parse).with(data) 18 | end 19 | 20 | it 'returns parser result' do 21 | expect(result).to equal(files) 22 | end 23 | end 24 | 25 | describe 'mapping files' do 26 | context 'with block' do 27 | let!(:result) { outline.map_files(parser, &:upcase) } 28 | 29 | it 'forwards data to parser' do 30 | expect(parser).to have_received(:parse).with(data) 31 | end 32 | 33 | it 'maps over parser result' do 34 | expect(result).to eq(["TWO", "FILES"]) 35 | end 36 | end 37 | 38 | context 'without block' do 39 | let!(:result) { outline.map_files(parser) } 40 | 41 | it 'returns result of kind Enumerator' do 42 | expect(result.class).to eq Enumerator 43 | end 44 | 45 | describe 'the result' do 46 | let!(:mapped_result) { result.map(&:upcase) } 47 | 48 | it 'forwards data to parser' do 49 | expect(parser).to have_received(:parse).with(data) 50 | end 51 | 52 | it 'enumerates over parse results' do 53 | expect(mapped_result).to eq ["TWO", "FILES"] 54 | end 55 | end 56 | end 57 | end 58 | end 59 | 60 | context 'with empty outline' do 61 | subject(:outline) { ZettelOutline::Outline.new("") } 62 | let(:parser) { double(:parse => nil) } 63 | 64 | describe 'file list' do 65 | let!(:result) { outline.files(parser) } 66 | 67 | it 'returns empty array' do 68 | expect(result).to eq [] 69 | end 70 | 71 | it "doesn't delegate to parser" do 72 | expect(parser).not_to have_received(:parse) 73 | end 74 | end 75 | 76 | describe 'mapping files' do 77 | context 'with block' do 78 | let!(:result) { outline.map_files(parser, &:upcase) } 79 | 80 | it 'does not delegate to parser' do 81 | expect(parser).not_to have_received(:parse) 82 | end 83 | 84 | it 'returns empty array' do 85 | expect(result).to eq([]) 86 | end 87 | end 88 | 89 | context 'without block' do 90 | let!(:result) { outline.map_files(parser) } 91 | 92 | it 'returns result of kind Enumerator' do 93 | expect(result.class).to eq Enumerator 94 | end 95 | 96 | describe 'the result' do 97 | let!(:mapped_result) { result.map(&:upcase) } 98 | 99 | it 'does not forward data to parser' do 100 | expect(parser).not_to have_received(:parse) 101 | end 102 | 103 | it 'is empty' do 104 | expect(mapped_result).to eq [] 105 | end 106 | end 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /spec/zettel_outline/zettel_info_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'zettel_outline/zettel_info' 3 | 4 | describe ZettelOutline::ZettelInfo do 5 | 6 | describe 'conventional id and title' do 7 | let(:line) { "201510101029 the title" } 8 | subject(:info) { ZettelOutline::ZettelInfo.new(line) } 9 | 10 | it 'strips the ID' do 11 | expect(subject.id).to eq "201510101029" 12 | end 13 | 14 | it 'strips the title' do 15 | expect(subject.title).to eq "the title" 16 | end 17 | 18 | it 'has no comment' do 19 | expect(subject.comment).to be_nil 20 | end 21 | end 22 | 23 | describe 'including comment' do 24 | let(:line) { "1234567 ze title. the comment" } 25 | subject(:info) { ZettelOutline::ZettelInfo.new(line) } 26 | 27 | it 'strips the ID' do 28 | expect(subject.id).to eq "1234567" 29 | end 30 | 31 | it 'strips the title' do 32 | expect(subject.title).to eq "ze title" 33 | end 34 | 35 | it 'strips the comment' do 36 | expect(subject.comment).to eq "the comment" 37 | end 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /spec/zettel_outline/zettel_renderer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'zettel_outline/zettel_renderer' 3 | 4 | describe ZettelOutline::ZettelRenderer do 5 | subject(:renderer) { ZettelOutline::ZettelRenderer.new } 6 | 7 | describe 'rendering' do 8 | let(:contents) { double(:render => "content") } 9 | let(:separator) { double(:render => "separator") } 10 | let!(:result) { renderer.render(contents, separator) } 11 | 12 | it 'renders contents' do 13 | expect(contents).to have_received(:render) 14 | end 15 | 16 | it 'renders separator' do 17 | expect(contents).to have_received(:render) 18 | end 19 | 20 | it 'returns rendition' do 21 | expect(result).to eq "separatorcontent\n\n" 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/zettel_outline/zettel_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'zettel_outline/zettel' 3 | 4 | class TestableZettel < ZettelOutline::Zettel 5 | attr_accessor :contents_double 6 | 7 | def initialize(contents) 8 | @contents_double = contents 9 | end 10 | 11 | def file_contents 12 | contents_double 13 | end 14 | end 15 | 16 | describe ZettelOutline::Zettel do 17 | describe 'initialization with nil' do 18 | it { expect { described_class.new(nil) }.to raise_exception "file path needed" } 19 | end 20 | 21 | describe 'initialization with path to directory' do 22 | it { expect { described_class.new("/tmp/") }.to raise_exception "file expected at path" } 23 | end 24 | 25 | describe 'initialization with path to non-existing file' do 26 | it { expect { described_class.new("/xtmp/foobar") }.to raise_exception "file expected at path" } 27 | end 28 | 29 | describe 'initialization with path to existing file' do 30 | let(:path) { "/tmp/201601221751-tempfile" } 31 | 32 | before(:each) do 33 | %x{touch "#{path}"} 34 | end 35 | 36 | it { expect { described_class.new(path) }.not_to raise_exception } 37 | it { expect(described_class.new(path)).not_to be_nil } 38 | end 39 | 40 | describe 'rendering' do 41 | let(:contents) { ["a", "b", "c"] } 42 | subject(:zettel) { TestableZettel.new(contents) } 43 | 44 | it 'returns file contents as one string' do 45 | expect(zettel.render).to eq "abc" 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /zettel_outline.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'zettel_outline/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "zettel_outline" 8 | spec.version = ZettelOutline::VERSION 9 | spec.authors = ["Christian Tietze"] 10 | spec.email = ["christian.tietze@gmail.com"] 11 | 12 | spec.summary = %q{Converts a plain text/Markdown outline of Zettel note references into a document.} 13 | spec.description = %q{Using outlines to plan writing projects is great, but assembling a first draft is still tiresome work. Adhere to this gem's outline convention and assemble first drafts in no time.} 14 | spec.homepage = "http://zettelkasten.de/" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 18 | spec.bindir = "exe" 19 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 20 | spec.require_paths = ["lib"] 21 | 22 | spec.add_development_dependency "bundler", "~> 2.1" 23 | spec.add_development_dependency "rake", "~> 13.0" 24 | spec.add_development_dependency "rspec", "~> 3.10" 25 | spec.add_runtime_dependency "slop", "~> 4.9" 26 | spec.add_runtime_dependency "colorize", "~> 0.8" 27 | end 28 | --------------------------------------------------------------------------------