├── .gitignore ├── portfolio-generator.gemspec ├── portfolio-generator.rb └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /portfolio-generator.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'portfolio-generator' 3 | s.version = '1.0.0' 4 | s.date = '2015-09-25' 5 | s.summary = "Portfolio generator for Jekyll" 6 | s.description = "Generates a portfolio/project pages (including related projects) out of data files" 7 | s.authors = ["Shannon Babincsak"] 8 | s.files = ["portfolio-generator.rb"] 9 | s.homepage = 'https://github.com/codeinpink/jekyll-portfolio-generator' 10 | end 11 | -------------------------------------------------------------------------------- /portfolio-generator.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | class ProjectPage < Page 3 | def initialize(site, base, dir, project_data) 4 | @site = site 5 | @base = base 6 | @dir = dir 7 | @name = "index.html" 8 | 9 | self.process(@name) 10 | self.read_yaml(File.join(base, "_layouts"), "project.html") 11 | 12 | project_data.each { |key, value| self.data[key] = value } 13 | end 14 | end 15 | 16 | class PortfolioGenerator < Generator 17 | safe true 18 | 19 | def generate(site) 20 | dir = site.config["portfolio_dir"] || "portfolio" 21 | 22 | # First get the related projects and add them to each project 23 | unless site.config["skip_related_projects"] == true 24 | raise ArgumentError.new "Missing related_project_keys in config file" unless site.config["related_project_keys"] 25 | compute_related_projects(site) 26 | end 27 | 28 | # Then generate the project pages 29 | site.data["projects"].each do |project_file| 30 | project = project_file[1] 31 | 32 | # I Love Cats -> i-love-cats 33 | file_name_slug = slugify(project["title"]) 34 | 35 | # portfolio/i-love-cats/ 36 | path = File.join(dir, file_name_slug) 37 | project["dir"] = path 38 | 39 | site.pages << ProjectPage.new(site, site.source, path, project) 40 | end 41 | end 42 | 43 | def compute_related_projects(site) 44 | projects = [] 45 | site.data["projects"].each { |project| projects.push(project[1]) } 46 | 47 | keys = site.config["related_project_keys"] 48 | 49 | projects.each do |project| 50 | project["related_projects"] = [] 51 | min = site.config["related_min_common"] || 0.6 52 | 53 | projects_copy = [] 54 | 55 | projects.clone.each do |copy_project| 56 | projects_copy.push([copy_project, get_matches(project, copy_project, keys)]) 57 | end 58 | 59 | related = projects_copy.keep_if { |project2| project2[1] >= min } 60 | related = related.sort { |related_project, related_project2| related_project[1] <=> related_project2[1] } 61 | related.reverse!.each { |related_project| project["related_projects"].push(related_project[0]) } 62 | end 63 | end 64 | 65 | def get_matches(project1, project2, keys) 66 | total = 0 67 | total_possible_matches = get_total_possible_matches(project1, keys) 68 | 69 | if project1.to_a == project2.to_a || total_possible_matches == 0 70 | return total 71 | else 72 | keys.each { |key| total += get_num_matches_for_key(project1, project2, key) } 73 | end 74 | 75 | result = total.fdiv(total_possible_matches).round(2) 76 | 77 | # Uncomment to see info about the matches for each project 78 | #puts "Matches between #{project1["title"]} and #{project2["title"]}: #{total} (#{result})" 79 | 80 | return result 81 | end 82 | 83 | def get_total_possible_matches(project, keys) 84 | total = 0 85 | 86 | keys.each do |key| 87 | if project[key].class == String 88 | total += 1 89 | else 90 | total += project[key].to_a.length() 91 | end 92 | end 93 | 94 | return total 95 | end 96 | 97 | def get_num_matches_for_key(project1, project2, key) 98 | matches = 0 99 | 100 | if project1[key].kind_of?(Array) 101 | matches = (project1[key] & project2[key]).length() 102 | else 103 | if project1[key] == project2[key] 104 | matches += 1 105 | end 106 | end 107 | 108 | return matches 109 | end 110 | 111 | def slugify(title) 112 | title.downcase.strip.gsub(' ', '-').gsub(/[^\w-]/, '') 113 | end 114 | 115 | end 116 | 117 | module ProjectFilter 118 | def get_projects_from_files(input) 119 | projects = [] 120 | input.each { |project| projects.push(project[1]) } 121 | return projects 122 | end 123 | end 124 | 125 | end 126 | 127 | Liquid::Template.register_filter(Jekyll::ProjectFilter) 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jekyll Portfolio Generator 2 | 3 | ## Getting Started 4 | 5 | ### Installation 6 | Install `portfolio_generator.rb` in your plugins directory (`_plugins`). 7 | 8 | ### Configuration 9 | Set your config options: 10 | 11 | * `portfolio_dir` - The directory in which the projects will be generated. Defaults 12 | to `portfolio`. 13 | 14 | * `skip_related_projects` - If `true`, it will not generate related projects for 15 | each project. Defaults to `false`. 16 | 17 | * `related_project_keys` - An array of project keys that will be used to compute 18 | related projects. This is **required** *if* you want to generate related projects. 19 | There is no default. 20 | 21 | * `related_min_common` - As a decimal, the minimum percentage of keys that should 22 | be common for a project to be considered related. Defaults to `0.6`. 23 | 24 | #### Sample `_config.yml`: 25 | 26 | ``` 27 | # ... 28 | 29 | portfolio_dir: "projects" 30 | 31 | related_project_keys: ["category", "technology"] 32 | 33 | related_min_common: 0.7 34 | 35 | # ... 36 | ``` 37 | 38 | ### Layout 39 | Create a layout file named `project.html` in your `_layouts` folder that will be used 40 | for the project pages. 41 | 42 | #### Sample `project.html`: 43 | 44 | ``` 45 |
{{ page.description }}
48 | 49 |