year . $post->month . $post->day; if ($date != $last_date) { $post->is_first_post_on_this_date = true; $last_date = $date; } $posts[] = $post; } return $posts; } public static function next_sequence_number_for_day($filename_prefix) { $max = 0; foreach (glob($filename_prefix . '*' . Updater::$post_extension) as $filename) { $n = intval(substr(substring_after($filename, $filename_prefix), 0, 2)); if ($n > $max) $max = $n; } return str_pad($max + 1, 2, '0', STR_PAD_LEFT); } public function __construct($source_filename, $is_draft = -1 /* auto */) { $this->source_filename = $source_filename; $this->is_draft = ($is_draft === -1 ? (false !== strpos($source_filename, 'drafts/') || false !== strpos($source_filename, 'pages/')) : $is_draft); $this->timestamp = filemtime($source_filename); $segments = preg_split( '/\R\R/', trim(file_get_contents($source_filename)), 2); if (! isset($segments[1])) $segments[1] = ''; if (count($segments) > 1) { // Read headers for Tag, Type values $headers = explode("\n", $segments[0]); $has_title_yet = false; foreach ($headers as $header) { if (isset($header[0]) && $header[0] == '=') { $has_title_yet = true; continue; } if (! $has_title_yet) { $has_title_yet = true; $this->title = $header; continue; } if ($this->is_draft && strtolower($header) == 'publish-now') { $this->publish_now = true; continue; } $fields = explode(':', $header, 2); if (count($fields) < 2) continue; $fname = strtolower($fields[0]); $fields[1] = trim($fields[1]); if ($fname == 'tags') { $this->tags = self::parse_tag_str($fields[1]); } else if ($fname == 'type') { $this->type = str_replace('|', ' ', $fields[1]); } else if ($fname == 'published') { $this->timestamp = strtotime($fields[1]); } else { $this->headers[$fname] = $fields[1]; } if (isset($this->headers['link'])) $this->type = 'link'; } array_shift($segments); } $this->body = isset($segments[0]) ? $segments[0] : ''; $filename = basename($source_filename); $filename_datestr = substr($filename, 0, 8); if (is_numeric($filename_datestr)) { $this->year = intval(substr($filename_datestr, 0, 4)); $this->month = intval(substr($filename_datestr, 4, 2)); $this->day = intval(substr($filename_datestr, 6, 2)); $this->offset_in_day = intval(substr($filename, 9, 2)); $this->slug = ltrim(substr($filename, 11, -(strlen(Updater::$post_extension))), '-'); } else { $this->year = intval(idate('Y', $this->timestamp)); $this->month = intval(idate('m', $this->timestamp)); $this->day = intval(idate('d', $this->timestamp)); $posts_in_ym = Updater::posts_in_year_month($this->year, $this->month); $this->offset_in_day = isset($posts_in_ym[$this->day]) ? count($posts_in_ym[$this->day]) + 1 : 1; $this->slug = substring_before($filename, '.'); } if (! $this->is_draft && $source_filename != $this->expected_source_filename()) { error_log("Expected filename mismatch:\nfilename: $source_filename\nexpected: " . $this->expected_source_filename()); } } public function expected_source_filename($for_new_file = false) { $padded_month = str_pad($this->month, 2, '0', STR_PAD_LEFT); $padded_day = str_pad($this->day, 2, '0', STR_PAD_LEFT); $prefix = Updater::$source_path . '/posts/' . $this->year . '/' . $padded_month . '/' . $this->year . $padded_month . $padded_day . '-' ; if ($for_new_file) $padded_offset = self::next_sequence_number_for_day($prefix); else $padded_offset = str_pad($this->offset_in_day, 2, '0', STR_PAD_LEFT); if ($for_new_file) { $slug = trim(preg_replace('/[^a-z0-9-]+/ms', '-', strtolower($this->slug)), '-'); if (! $slug) $slug = 'post'; } else { $slug = $this->slug; } return $prefix . $padded_offset . '-' . strtolower($slug) . Updater::$post_extension; } public function normalized_source() { $out_headers = $this->headers; if ($this->is_draft) unset($out_headers['published']); else $out_headers['published'] = date('Y-m-d h:i:sa', $this->timestamp); $source = $this->title . "\n" . str_repeat('=', max(10, min(40, strlen($this->title)))) . "\n"; if ($this->type) $out_headers['type'] = $this->type; if ($this->tags) $out_headers['tags'] = implode(', ', $this->tags); foreach ($out_headers as $k => $v) $source .= ucfirst($k) . ': ' . $v . "\n"; $source .= "\n" . $this->body; return $source; } public function array_for_template() { $tags = array(); foreach ($this->tags as $tag) { $tags[$tag] = array('post-tag' => $tag); } // Convert relative image references to absolute so index pages work $base_uri = '/' . $this->year . '/' . str_pad($this->month, 2, '0', STR_PAD_LEFT) . '/' . str_pad($this->day, 2, '0', STR_PAD_LEFT); return array_merge( $this->headers, array( 'post-title' => html_entity_decode(SmartyPants($this->title), ENT_QUOTES, 'UTF-8'), 'post-slug' => $this->slug, 'post-timestamp' => $this->timestamp, 'post-rss-date' => date('D, d M Y H:i:s T', $this->timestamp), 'post-body' => SmartyPants(Markdown($this->body)), 'post-tags' => $tags, 'post-type' => $this->type, 'post-permalink' => $base_uri . '/' . $this->slug, 'post-permalink-or-link' => isset($this->headers['link']) && $this->headers['link'] ? $this->headers['link'] : $base_uri . '/' . $this->slug, 'post-absolute-permalink' => rtrim(self::$blog_url, '/') . $base_uri . '/' . $this->slug, 'post-absolute-permalink-or-link' => rtrim(self::$blog_url, '/') . (isset($this->headers['link']) && $this->headers['link'] ? $this->headers['link'] : $base_uri . '/' . $this->slug), 'post-is-first-on-date' => $this->is_first_post_on_this_date ? 'yes' : '', ) ); } public function write_page() { $t = new Template(Updater::$page_template); $t->content = array( 'page-title' => html_entity_decode(SmartyPants($this->title), ENT_QUOTES, 'UTF-8'), 'page-body' => SmartyPants(Markdown($this->body)), 'blog-title' => html_entity_decode(SmartyPants(self::$blog_title), ENT_QUOTES, 'UTF-8'), 'blog-url' => self::$blog_url, 'blog-description' => html_entity_decode(SmartyPants(self::$blog_description), ENT_QUOTES, 'UTF-8'), 'page-type' => 'page', 'archives' => array(), 'previous_page_url' => false, 'next_page_url' => false, ); $output_html = $t->outputHTML(); if (! file_exists(Updater::$dest_path)) mkdir_as_parent_owner(Updater::$dest_path, 0755, true); file_put_contents_as_dir_owner(Updater::$dest_path . '/' . $this->slug, $output_html); } public function write_permalink_page($draft = false) { $post_data = $this->array_for_template(); $post_data['post-is-first-on-date'] = true; $t = new Template(Updater::$permalink_template); $t->content = array_merge( $post_data, array( 'page-title' => html_entity_decode(SmartyPants(self::$blog_title), ENT_QUOTES, 'UTF-8'), 'blog-title' => html_entity_decode(SmartyPants(self::$blog_title), ENT_QUOTES, 'UTF-8'), 'blog-url' => self::$blog_url, 'blog-description' => html_entity_decode(SmartyPants(self::$blog_description), ENT_QUOTES, 'UTF-8'), 'page-type' => 'post', 'posts' => array($post_data), 'post' => $post_data, 'archives' => array(), 'previous_page_url' => false, 'next_page_url' => false, ) ); $output_html = $t->outputHTML(); if (! $draft || Updater::$write_public_drafts) { $output_www_path = $draft ? '/drafts' : dirname($post_data['post-permalink']); $output_path = Updater::$dest_path . $output_www_path; if (! file_exists($output_path)) mkdir_as_parent_owner($output_path, 0755, true); file_put_contents_as_dir_owner(Updater::$dest_path . ($draft ? '/drafts/' . $this->slug : $post_data['post-permalink']), $output_html); } if ($draft) file_put_contents_as_dir_owner(Updater::$source_path . '/drafts/_previews/' . $this->slug . '.html', $output_html); } public static function write_index($dest_path, $title, $type, array $posts, $template = 'main.html', $archive_array = false, $seq_count = 0) { $posts_data = array(); foreach ($posts as $p) $posts_data[] = $p->array_for_template(); $t = new Template($template); $t->content = array( 'page-title' => html_entity_decode(SmartyPants($title), ENT_QUOTES, 'UTF-8'), 'blog-title' => html_entity_decode(SmartyPants(self::$blog_title), ENT_QUOTES, 'UTF-8'), 'blog-url' => self::$blog_url, 'blog-description' => html_entity_decode(SmartyPants(self::$blog_description), ENT_QUOTES, 'UTF-8'), 'page-type' => $type, 'posts' => $posts_data, 'previous_page_url' => false, 'next_page_url' => $seq_count > 1 ? substring_after(substring_before($dest_path, '.', true), Updater::$dest_path) . '-2' : false, 'archives' => $archive_array ? $archive_array : array(), ); $output_html = $t->outputHTML(); $output_path = dirname($dest_path); if (! file_exists($output_path)) mkdir_as_parent_owner($output_path, 0755, true); file_put_contents_as_dir_owner($dest_path, $output_html); } public function write_index_sequence($dest_path, $title, $type, array $posts, $template = 'main.html', $archive_array = false, $posts_per_page = 20) { $sequence = 0; $new_dest_path = $dest_path; // $dest_uri = substring_after(substring_before($dest_path, '.', true), Updater::$dest_path); $dest_uri = substring_after(substring_after($dest_path, '/', true), Updater::$dest_path); $total_sequences = ceil(count($posts) / $posts_per_page); while ($posts) { $sequence++; $seq_array = array_splice($posts, 0, $posts_per_page); if ($sequence == 1) continue; // skip writing the redundant "-1" page $new_dest_path = $dest_path . '-' . $sequence . '.html'; $posts_data = array(); foreach ($seq_array as $p) $posts_data[] = $p->array_for_template(); $t = new Template($template); $t->content = array( 'page-title' => html_entity_decode(SmartyPants($title), ENT_QUOTES, 'UTF-8'), 'blog-title' => html_entity_decode(SmartyPants(self::$blog_title), ENT_QUOTES, 'UTF-8'), 'blog-url' => self::$blog_url, 'blog-description' => html_entity_decode(SmartyPants(self::$blog_description), ENT_QUOTES, 'UTF-8'), 'page-type' => $type, 'posts' => $posts_data, 'previous_page_url' => $sequence != 1 ? ($sequence == 2 ? $dest_uri : $dest_uri . '-' . ($sequence - 1)) : false, 'next_page_url' => $sequence < $total_sequences ? $dest_uri . '-' . ($sequence + 1) : false, 'archives' => $archive_array ? $archive_array : array(), ); $output_html = $t->outputHTML(); $output_path = dirname($new_dest_path); if (! file_exists($output_path)) mkdir_as_parent_owner($output_path, 0755, true); file_put_contents_as_dir_owner($new_dest_path, $output_html); } return $total_sequences; } public static function parse_tag_str($tag_str) { // Tags are comma-separated, and any spaces between multiple words in a tag will be converted to underscores for URLs if (! strlen($tag_str)) return array(); $tags = array_map('trim', explode(',', strtolower($tag_str))); $tags = str_replace(' ', '_', $tags); $tags = array_unique(array_filter($tags, 'strlen')); sort($tags); return $tags; } }