diff --git a/.rubocop.yml b/.rubocop.yml index 15bcb5df..b0fef47a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -16,7 +16,7 @@ Lint/UnreachableCode: Lint/UselessAccessModifier: Enabled: false Metrics/AbcSize: - Max: 20 + Max: 21 Metrics/ClassLength: Exclude: - !ruby/regexp /features\/.*.rb$/ diff --git a/features/theme.feature b/features/theme.feature index 0e05d693..7729a0eb 100644 --- a/features/theme.feature +++ b/features/theme.feature @@ -17,7 +17,7 @@ Feature: Writing themes Scenario: A theme with SCSS Given I have a configuration file with "theme" set to "test-theme" And I have a css directory - And I have a "css/main.scss" page that contains "@import 'style';" + And I have a "css/main.scss" page that contains "@import 'test-theme-black';" When I run jekyll build Then I should get a zero exit status And the _site directory should exist diff --git a/lib/jekyll.rb b/lib/jekyll.rb index a9fe696f..3c4def1a 100644 --- a/lib/jekyll.rb +++ b/lib/jekyll.rb @@ -55,6 +55,7 @@ module Jekyll autoload :PostReader, "jekyll/readers/post_reader" autoload :PageReader, "jekyll/readers/page_reader" autoload :StaticFileReader, "jekyll/readers/static_file_reader" + autoload :ThemeAssetsReader, "jekyll/readers/theme_assets_reader" autoload :LogAdapter, "jekyll/log_adapter" autoload :Page, "jekyll/page" autoload :PluginManager, "jekyll/plugin_manager" diff --git a/lib/jekyll/page.rb b/lib/jekyll/page.rb index 4e3efe3f..3e3c7ec8 100644 --- a/lib/jekyll/page.rb +++ b/lib/jekyll/page.rb @@ -40,7 +40,11 @@ module Jekyll @base = base @dir = dir @name = name - @path = site.in_source_dir(base, dir, name) + @path = if site.in_theme_dir(base) == base # we're in a theme + site.in_theme_dir(base, dir, name) + else + site.in_source_dir(base, dir, name) + end process(name) read_yaml(File.join(base, dir), name) diff --git a/lib/jekyll/reader.rb b/lib/jekyll/reader.rb index abbe923f..7bde499d 100644 --- a/lib/jekyll/reader.rb +++ b/lib/jekyll/reader.rb @@ -18,6 +18,7 @@ module Jekyll sort_files! @site.data = DataReader.new(site).read(site.config["data_dir"]) CollectionReader.new(site).read + ThemeAssetsReader.new(site).read end # Sorts posts, pages, and static files. diff --git a/lib/jekyll/readers/theme_assets_reader.rb b/lib/jekyll/readers/theme_assets_reader.rb new file mode 100644 index 00000000..035c0607 --- /dev/null +++ b/lib/jekyll/readers/theme_assets_reader.rb @@ -0,0 +1,47 @@ +module Jekyll + class ThemeAssetsReader + attr_reader :site + def initialize(site) + @site = site + end + + def read + return unless site.theme && site.theme.assets_path + + Find.find(site.theme.assets_path) do |path| + next if File.directory?(path) + if File.symlink?(path) + Jekyll.logger.warn "Theme reader:", "Ignored symlinked asset: #{path}" + else + read_theme_asset(path) + end + end + end + + private + def read_theme_asset(path) + base = site.theme.root + dir = File.dirname(path.sub("#{site.theme.root}/", "")) + name = File.basename(path) + + if Utils.has_yaml_header?(path) + append_unless_exists site.pages, + Jekyll::Page.new(site, base, dir, name) + else + append_unless_exists site.static_files, + Jekyll::StaticFile.new(site, base, dir, name) + end + end + + def append_unless_exists(haystack, new_item) + if haystack.any? { |file| file.relative_path == new_item.relative_path } + Jekyll.logger.debug "Theme:", + "Ignoring #{new_item.relative_path} in theme due to existing file " \ + "with that path in site." + return + end + + haystack << new_item + end + end +end diff --git a/lib/jekyll/theme.rb b/lib/jekyll/theme.rb index 0dd73f78..803004f3 100644 --- a/lib/jekyll/theme.rb +++ b/lib/jekyll/theme.rb @@ -18,15 +18,19 @@ module Jekyll end def includes_path - path_for :includes + path_for "_includes".freeze end def layouts_path - path_for :layouts + path_for "_layouts".freeze end def sass_path - path_for :sass + path_for "_sass".freeze + end + + def assets_path + path_for "assets".freeze end def configure_sass @@ -43,7 +47,7 @@ module Jekyll end def realpath_for(folder) - File.realpath(Jekyll.sanitized_path(root, "_#{folder}")) + File.realpath(Jekyll.sanitized_path(root, folder.to_s)) rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP nil end diff --git a/lib/jekyll/theme_builder.rb b/lib/jekyll/theme_builder.rb index 33477185..68a5eeab 100644 --- a/lib/jekyll/theme_builder.rb +++ b/lib/jekyll/theme_builder.rb @@ -1,6 +1,6 @@ class Jekyll::ThemeBuilder SCAFFOLD_DIRECTORIES = %w( - _layouts _includes _sass + assets _layouts _includes _sass ).freeze attr_reader :name, :path, :code_of_conduct diff --git a/site/_docs/themes.md b/site/_docs/themes.md index 988f6ebd..c7d6d574 100644 --- a/site/_docs/themes.md +++ b/site/_docs/themes.md @@ -27,6 +27,7 @@ Jekyll themes set default layouts, includes, and stylesheets, that can be overri Jekyll will look first to your site's content, before looking to the theme's defaults, for any requested file in the following folders: +* `/assets` * `/_layouts` * `/_includes` * `/_sass` @@ -68,6 +69,12 @@ Theme layouts and includes work just like they work in any Jekyll site. Place la For example, if your theme has a `/_layouts/page.html` file, and a page has `layout: page` in its YAML front matter, Jekyll will first look to the site's `_layouts` folder for a the `page` layout, and if none exists, will use your theme's `page` layout. +### Assets + +Any file in `/assets` will be copied over to the user's site upon build unless they have a file with the same relative path. You may ship any kind of asset here: SCSS, an image, a webfont, etc. These files behave just like pages and static files in Jekyll: if the file has [YAML front matter]({{ site.baseurl }}/docs/frontmatter/) at the top, then it will be rendered. If it does not have YAML front matter, it will simply be copied over into the resulting site. This allows theme creators to ship a default `/assets/styles.scss` file which their layouts can depend on as `/assets/styles.css`. + +All files in `/assets` will be output into the compiled site in the `/assets` folder just as you'd expect from using Jekyll on your sites. + ### Stylesheets Your theme's stylesheets should be placed in your theme's `/_sass` folder, again, just as you would when authoring a Jekyll site. Your theme's styles can be included in the user's stylesheet using the `@import` directive. diff --git a/test/fixtures/test-theme/_sass/style.scss b/test/fixtures/test-theme/_sass/test-theme-black.scss similarity index 100% rename from test/fixtures/test-theme/_sass/style.scss rename to test/fixtures/test-theme/_sass/test-theme-black.scss diff --git a/test/fixtures/test-theme/_sass/test-theme-red.scss b/test/fixtures/test-theme/_sass/test-theme-red.scss new file mode 100644 index 00000000..0307e17a --- /dev/null +++ b/test/fixtures/test-theme/_sass/test-theme-red.scss @@ -0,0 +1,3 @@ +.sample { + color: red; +} diff --git a/test/fixtures/test-theme/assets/application.coffee b/test/fixtures/test-theme/assets/application.coffee new file mode 100644 index 00000000..02f33515 --- /dev/null +++ b/test/fixtures/test-theme/assets/application.coffee @@ -0,0 +1,3 @@ +--- +--- +alert "From your theme." diff --git a/test/fixtures/test-theme/assets/img/another-logo.png b/test/fixtures/test-theme/assets/img/another-logo.png new file mode 120000 index 00000000..bd36e718 --- /dev/null +++ b/test/fixtures/test-theme/assets/img/another-logo.png @@ -0,0 +1 @@ +logo.png \ No newline at end of file diff --git a/test/fixtures/test-theme/assets/img/logo.png b/test/fixtures/test-theme/assets/img/logo.png new file mode 100644 index 00000000..0d1cbe53 Binary files /dev/null and b/test/fixtures/test-theme/assets/img/logo.png differ diff --git a/test/fixtures/test-theme/assets/style.scss b/test/fixtures/test-theme/assets/style.scss new file mode 100644 index 00000000..47c4a2f1 --- /dev/null +++ b/test/fixtures/test-theme/assets/style.scss @@ -0,0 +1,3 @@ +--- +--- +@import "test-theme-{{ site.theme-color | default: "red" }}"; diff --git a/test/source/assets/application.coffee b/test/source/assets/application.coffee new file mode 100644 index 00000000..b9f5e1ef --- /dev/null +++ b/test/source/assets/application.coffee @@ -0,0 +1,3 @@ +--- +--- +alert "From your site." diff --git a/test/test_filters.rb b/test/test_filters.rb index b5f0a395..d6ff9eac 100644 --- a/test/test_filters.rb +++ b/test/test_filters.rb @@ -475,7 +475,7 @@ class TestFilters < JekyllUnitTest g["items"].is_a?(Array), "The list of grouped items for '' is not an Array." ) - assert_equal 14, g["items"].size + assert_equal 15, g["items"].size end end end diff --git a/test/test_site.rb b/test/test_site.rb index 83d6e456..1f0d30a1 100644 --- a/test/test_site.rb +++ b/test/test_site.rb @@ -180,6 +180,7 @@ class TestSite < JekyllUnitTest %#\ +.md .htaccess about.html + application.coffee bar.html coffeescript.coffee contacts.html diff --git a/test/test_theme.rb b/test/test_theme.rb index a448ce30..fd380d95 100644 --- a/test/test_theme.rb +++ b/test/test_theme.rb @@ -34,16 +34,16 @@ class TestTheme < JekyllUnitTest end context "path generation" do - [:layouts, :includes, :sass].each do |folder| + [:assets, :_layouts, :_includes, :_sass].each do |folder| should "know the #{folder} path" do - expected = File.expand_path("_#{folder}", @expected_root) - assert_equal expected, @theme.public_send("#{folder}_path") + expected = File.expand_path(folder.to_s, @expected_root) + assert_equal expected, @theme.public_send("#{folder.to_s.tr("_", "")}_path") end end should "generate folder paths" do expected = File.expand_path("./_sass", @expected_root) - assert_equal expected, @theme.send(:path_for, :sass) + assert_equal expected, @theme.send(:path_for, :_sass) end should "not allow paths outside of the theme root" do @@ -56,7 +56,7 @@ class TestTheme < JekyllUnitTest should "return the resolved path when a symlink & resolved path exists" do expected = File.expand_path("./_layouts", @expected_root) - assert_equal expected, @theme.send(:path_for, :symlink) + assert_equal expected, @theme.send(:path_for, :_symlink) end end diff --git a/test/test_theme_assets_reader.rb b/test/test_theme_assets_reader.rb new file mode 100644 index 00000000..fc335248 --- /dev/null +++ b/test/test_theme_assets_reader.rb @@ -0,0 +1,71 @@ +require "helper" + +class TestThemeAssetsReader < JekyllUnitTest + def setup + @site = fixture_site( + "theme" => "test-theme", + "theme-color" => "black" + ) + assert @site.theme + end + + def assert_file_with_relative_path(haystack, relative_path) + assert haystack.any? { |f| + f.relative_path == relative_path + }, "Site should read in the #{relative_path} file, " \ + "but it was not found in #{haystack.inspect}" + end + + def refute_file_with_relative_path(haystack, relative_path) + refute haystack.any? { |f| + f.relative_path == relative_path + }, "Site should not have read in the #{relative_path} file, " \ + "but it was found in #{haystack.inspect}" + end + + context "with a valid theme" do + should "read all assets" do + @site.reset + ThemeAssetsReader.new(@site).read + assert_file_with_relative_path @site.static_files, "assets/img/logo.png" + assert_file_with_relative_path @site.pages, "assets/style.scss" + end + + should "convert pages" do + @site.process + + file = @site.pages.find { |f| f.relative_path == "assets/style.scss" } + refute_nil file + assert_equal @site.in_dest_dir("assets/style.css"), file.destination(@site.dest) + assert_includes file.output, ".sample {\n color: black; }" + end + + should "not overwrite site content with the same relative path" do + @site.reset + @site.read + + file = @site.pages.find { |f| f.relative_path == "assets/application.coffee" } + refute_nil file + assert_includes file.content, "alert \"From your site.\"" + end + end + + context "with a valid theme without an assets dir" do + should "not read any assets" do + site = fixture_site("theme" => "test-theme") + allow(site.theme).to receive(:assets_path).and_return(nil) + ThemeAssetsReader.new(site).read + refute_file_with_relative_path site.static_files, "assets/img/logo.png" + refute_file_with_relative_path site.pages, "assets/style.scss" + end + end + + context "with no theme" do + should "not read any assets" do + site = fixture_site("theme" => nil) + ThemeAssetsReader.new(site).read + refute_file_with_relative_path site.static_files, "assets/img/logo.png" + refute_file_with_relative_path site.pages, "assets/style.scss" + end + end +end