├── sample ├── _theme │ ├── footer.twig │ ├── index.twig │ ├── header.twig │ ├── post.twig │ └── archives.twig ├── posts │ └── hello-world.md ├── config.yaml └── _public │ └── style.css ├── test.php ├── .gitignore ├── bin └── logecho ├── composer.json ├── lib ├── functions.php └── Atom.php └── workflow ├── import.php ├── main.php └── compile.php /sample/_theme/footer.twig: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample/posts/hello-world.md: -------------------------------------------------------------------------------- 1 | @tag: hello 2 | @category: default 3 | @date:2014-05-31 4 | 5 | Hello World 6 | =========== 7 | 8 | This is your first post. 9 | -------------------------------------------------------------------------------- /test.php: -------------------------------------------------------------------------------- 1 | 4 | {% for post in index.post %} 5 |
6 |

{{post.title}}

7 |
8 | 9 |
10 |
11 |
12 | {{post.content | more(300)}} 13 |
14 |
15 | {% endfor %} 16 | 17 | - Archives - 18 | 19 | {% include "footer.twig" %} 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /sample/_theme/header.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{title}} 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 19 | 26 |
27 | -------------------------------------------------------------------------------- /sample/config.yaml: -------------------------------------------------------------------------------- 1 | # You can configure this file to customize your site 2 | globals: 3 | # Definition of global variable 4 | title : My Blog Site 5 | description : Yet another logecho site 6 | url : http://logecho.com/ 7 | blocks: 8 | # All blocks to compile 9 | post: 10 | source : /posts/ 11 | target : /posts/ 12 | category: 13 | source : 14 | default : Default 15 | template : archives.twig 16 | target : archives.html 17 | tag: 18 | template : archives.twig 19 | target : archives.html 20 | archive: 21 | template : archives.twig 22 | target : archives.html 23 | index: 24 | template : index.twig 25 | target : index.html 26 | limit : 10 27 | feeds: 28 | # Definition of feeds gererating 29 | source : post 30 | limit : 20 31 | target : feeds.xml 32 | author : Logecho 33 | 34 | sync: http://your-custom-key@example.com 35 | 36 | -------------------------------------------------------------------------------- /sample/_theme/post.twig: -------------------------------------------------------------------------------- 1 | {% include "header.twig" %} 2 | 3 |
4 |
5 |

{{post.title}}

6 | 15 |
16 |
17 | {{post.content}} 18 |
19 | {% if post.tag is not empty %} 20 | 28 | {% endif %} 29 |
30 | 31 | - Archives - 32 | 33 | {% include "footer.twig" %} 34 |
35 |
36 | 37 | -------------------------------------------------------------------------------- /sample/_theme/archives.twig: -------------------------------------------------------------------------------- 1 | {% include "header.twig" %} 2 | 3 |
4 |
5 |

Archives

6 |
7 |
8 | {% for item in archive %} 9 |

{{item.name}}

10 |
    11 | {% for post in item.post %} 12 |
  • {{post.title}}
  • 13 | {% endfor %} 14 |
15 | {% endfor %} 16 |
17 |
18 | 19 |
20 |

Categories

21 |
22 |
23 | {% for item in category %} 24 |

{{item.name}}

25 |
    26 | {% for post in item.post %} 27 |
  • {{post.title}}
  • 28 | {% endfor %} 29 |
30 | {% endfor %} 31 |
32 |
33 |
34 |

Tags

35 |
36 |
37 | {% for item in tag %} 38 |

{{item.name}}

39 |
    40 | {% for post in item.post %} 41 |
  • {{post.title}}
  • 42 | {% endfor %} 43 |
44 | {% endfor %} 45 |
46 |
47 | 48 | {% include "footer.twig" %} 49 |
50 | 51 | 52 | -------------------------------------------------------------------------------- /sample/_public/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Georgia, "Times New Roman", Times, "STSong","NSimSun",serif; 3 | line-height: 1.6; 4 | color: #333; 5 | } 6 | 7 | a { 8 | color: blue; 9 | text-decoration: none; 10 | } 11 | a:hover { 12 | text-decoration: underline; 13 | } 14 | 15 | h1, h2, h3, h4, h5, h6 { 16 | line-height: 1.25; 17 | } 18 | h1, .h1 { font-size: 32px; } 19 | h2, .h2 { font-size: 28px; } 20 | h3, .h3 { font-size: 24px; } 21 | h4, .h4 { font-size: 18px; } 22 | h5, .h5 { font-size: 16px; } 23 | h6, .h6 { font-size: 14px; } 24 | 25 | pre, code { 26 | background: #F0F0F0; 27 | font-family: Monaco, 'Courier New', Courier, monospace; 28 | } 29 | 30 | pre { 31 | padding: 20px; 32 | } 33 | 34 | code { 35 | padding: 0 5px; 36 | } 37 | 38 | img { 39 | max-width: 100%; 40 | } 41 | 42 | 43 | /** 44 | * layout 45 | */ 46 | .container { 47 | margin: 0 auto; 48 | max-width: 1140px; 49 | } 50 | 51 | .col-main, 52 | .col-secondary { 53 | float: left; 54 | padding: 15px; 55 | -webkit-box-sizing: border-box; 56 | -moz-box-sizing: border-box; 57 | box-sizing: border-box; 58 | } 59 | 60 | .col-main { 61 | padding-left: 0; 62 | border-left: 1px solid #eee; 63 | width: 75%; 64 | } 65 | 66 | .col-secondary { 67 | padding-right: 40px; 68 | width: 25%; 69 | } 70 | 71 | 72 | /** 73 | * navigation 74 | */ 75 | .site-header { 76 | margin-top: 40px; 77 | margin-bottom: 40px; 78 | } 79 | .site-logo { 80 | font-size: 64px; 81 | background-color: #333; 82 | color: #fff; 83 | display: inline-block; 84 | text-align: center; 85 | line-height: 128px; 86 | width: 128px; 87 | height: 128px; 88 | border-radius: 128px; 89 | border: 3px solid #fff; 90 | box-shadow: 0 0 0 5px #333; 91 | } 92 | .site-logo:hover { 93 | text-decoration: none; 94 | } 95 | 96 | .site-name { 97 | margin: 1em 0 -0.4em; 98 | } 99 | .site-name a { 100 | color: #333; 101 | } 102 | .site-bio { 103 | color: #999; 104 | } 105 | .site-nav { 106 | padding: 0; 107 | list-style: none; 108 | } 109 | .site-nav li { 110 | margin: 10px 0; 111 | } 112 | 113 | .site-nav a { 114 | display: inline-block; 115 | padding: 2px 10px; 116 | border: 3px solid #eee; 117 | border-radius: 50px; 118 | } 119 | 120 | .site-nav a:hover { 121 | border-color: blue; 122 | text-decoration: none; 123 | } 124 | 125 | 126 | /** 127 | * post 128 | */ 129 | .post { 130 | padding: 20px 0 20px 40px; 131 | border-bottom: 1px solid #eee; 132 | } 133 | .post__title { 134 | margin-bottom: 5px; 135 | } 136 | .post__title a { 137 | color: #333; 138 | } 139 | .post__title a:hover { 140 | } 141 | .post__meta { 142 | color: #999; 143 | } 144 | .post__tag { 145 | margin: 1em 0; 146 | } 147 | .post__tag ul { 148 | display: inline; 149 | padding: 0; 150 | } 151 | .post__tag li { 152 | display: inline-block; 153 | color: #999; 154 | } 155 | .post__tag li:after { 156 | content: ","; 157 | } 158 | .post__tag li:last-child:after { 159 | content: none; 160 | } 161 | 162 | /* page */ 163 | .page { 164 | border-bottom: none; 165 | } 166 | .hr-title { 167 | margin: 20px 0; 168 | border: 1px solid #eee; 169 | border-width: 1px 0 0 0; 170 | } 171 | 172 | 173 | .btn-archive { 174 | display: block; 175 | padding: 40px 0 40px 40px; 176 | text-align: center; 177 | font-size: 18px; 178 | } 179 | 180 | .site-footer { 181 | padding: 20px 0 20px 40px; 182 | border-top: 1px solid #eee; 183 | text-align: center; 184 | font-style: italic; 185 | } 186 | 187 | 188 | /** 189 | * clearfix 190 | */ 191 | .clearfix:before, 192 | .clearfix:after { 193 | content: " "; // 1 194 | display: table; // 2 195 | } 196 | .clearfix:after { 197 | clear: both; 198 | } -------------------------------------------------------------------------------- /lib/functions.php: -------------------------------------------------------------------------------- 1 | '33', 23 | 'done' => '32', 24 | 'error' => '31', 25 | 'debug' => '36' 26 | ]; 27 | 28 | if (!__DEBUG__ && $type == 'debug') { 29 | return; 30 | } 31 | 32 | $color = isset($colors[$type]) ? $colors[$type] : '37'; 33 | 34 | $args = array_slice(func_get_args(), 2); 35 | array_unshift($args, $str); 36 | $str = call_user_func_array('sprintf', $args); 37 | 38 | echo "\033[{$color};1m[" . $type . "]\t\033[37;0m {$str}\n"; 39 | } 40 | 41 | /** 42 | * trigger a fatal error 43 | * 44 | * @param string $str 45 | * @throws Exception 46 | */ 47 | function le_fatal($str) { 48 | $args = func_get_args(); 49 | throw new Exception(call_user_func_array('sprintf', $args)); 50 | } 51 | 52 | // init global vars 53 | global $workflow, $context; 54 | $workflow = []; 55 | $context = new stdClass(); 56 | 57 | /** 58 | * get current caller file 59 | * 60 | * @return string 61 | */ 62 | function le_get_current_namespace() { 63 | static $file; 64 | 65 | $traces = debug_backtrace(!DEBUG_BACKTRACE_IGNORE_ARGS & !DEBUG_BACKTRACE_PROVIDE_OBJECT, 2); 66 | $current = array_pop($traces); 67 | 68 | $file = isset($current['file']) ? $current['file'] : $file; 69 | 70 | return pathinfo($file, PATHINFO_FILENAME); 71 | } 72 | 73 | /** 74 | * get a dir recursive iterator 75 | * 76 | * @param string $dir 77 | * @return RecursiveIteratorIterator 78 | */ 79 | function le_get_all_files($dir) { 80 | return !is_dir($dir) ? [] : new RecursiveIteratorIterator(new LEIgnorantRecursiveDirectoryIterator($dir, 81 | FilesystemIterator::KEY_AS_FILENAME 82 | | FilesystemIterator::CURRENT_AS_PATHNAME | FilesystemIterator::SKIP_DOTS)); 83 | } 84 | 85 | class LEIgnorantRecursiveDirectoryIterator extends RecursiveDirectoryIterator { 86 | function getChildren() { 87 | try { 88 | return new LEIgnorantRecursiveDirectoryIterator($this->getPathname(), FilesystemIterator::KEY_AS_FILENAME 89 | | FilesystemIterator::CURRENT_AS_PATHNAME | FilesystemIterator::SKIP_DOTS); 90 | } catch(UnexpectedValueException $e) { 91 | return new RecursiveArrayIterator(array()); 92 | } 93 | } 94 | } 95 | 96 | /** 97 | * add workflow 98 | * 99 | * @param string $name 100 | * @param mixed $func 101 | */ 102 | function le_add_workflow($name, $func) { 103 | global $workflow; 104 | 105 | $ns = le_get_current_namespace(); 106 | $workflow[$ns . '.' . $name] = $func; 107 | } 108 | 109 | /** 110 | * @param $name 111 | * @return mixed 112 | */ 113 | function le_do_workflow($name) { 114 | global $workflow, $context; 115 | 116 | $args = func_get_args(); 117 | array_shift($args); 118 | 119 | $parts = explode('.', $name, 2); 120 | if (2 == count($parts)) { 121 | list ($ns) = $parts; 122 | } else { 123 | $ns = le_get_current_namespace(); 124 | $name = $ns . '.' . $name; 125 | } 126 | 127 | require_once __DIR__ . '/../workflow/' . $ns . '.php'; 128 | 129 | if (!isset($workflow[$name])) { 130 | le_fatal('can not find workflow "%s"', $name); 131 | } 132 | 133 | $desc = implode(', ', array_map(function ($arg) { 134 | return is_string($arg) ? mb_strimwidth( 135 | str_replace(["\r", "\n"], '', $arg) 136 | , 0, 10, '...', 'UTF-8') : '...'; 137 | }, $args)); 138 | 139 | le_console('debug', '%s%s', $name, empty($desc) ? '' : ': ' . $desc); 140 | return call_user_func_array($workflow[$name], $args); 141 | } 142 | 143 | -------------------------------------------------------------------------------- /lib/Atom.php: -------------------------------------------------------------------------------- 1 | _charset = $charset; 81 | $this->_special = array_map('chr', range(0, 8)); 82 | } 83 | 84 | /** 85 | * 去掉一些特殊字符 86 | * 87 | * @param mixed $str 88 | * @access private 89 | * @return string 90 | */ 91 | private function encode($str) 92 | { 93 | return str_replace($this->_special, '', htmlspecialchars($str, ENT_IGNORE, 'UTF-8')); 94 | } 95 | 96 | /** 97 | * 设置标题 98 | * 99 | * @access public 100 | * @param string $title 标题 101 | * @return void 102 | */ 103 | public function setTitle($title) 104 | { 105 | $this->_title = $title; 106 | } 107 | 108 | /** 109 | * 设置副标题 110 | * 111 | * @access public 112 | * @param string $subTitle 副标题 113 | * @return void 114 | */ 115 | public function setSubTitle($subTitle) 116 | { 117 | $this->_subTitle = $subTitle; 118 | } 119 | 120 | /** 121 | * 设置聚合地址 122 | * 123 | * @access public 124 | * @param string $feedUrl 聚合地址 125 | * @return void 126 | */ 127 | public function setFeedUrl($feedUrl) 128 | { 129 | $this->_feedUrl = $feedUrl; 130 | } 131 | 132 | /** 133 | * 设置主页 134 | * 135 | * @access public 136 | * @param string $baseUrl 主页地址 137 | * @return void 138 | */ 139 | public function setBaseUrl($baseUrl) 140 | { 141 | $this->_baseUrl = $baseUrl; 142 | } 143 | 144 | /** 145 | * $item的格式为 146 | * 147 | * array ( 148 | * 'title' => 'xxx', 149 | * 'content' => 'xxx', 150 | * 'excerpt' => 'xxx', 151 | * 'date' => 'xxx', 152 | * 'link' => 'xxx', 153 | * 'author' => 'xxx', 154 | * 'comments' => 'xxx', 155 | * ) 156 | * 157 | * 158 | * @access public 159 | * @param array $item 160 | * @return unknown 161 | */ 162 | public function addItem(array $item) 163 | { 164 | $this->_items[] = $item; 165 | } 166 | 167 | /** 168 | * 输出字符串 169 | * 170 | * @access public 171 | * @return string 172 | */ 173 | public function generate() 174 | { 175 | $result = '_charset . '"?>' . self::EOL; 176 | 177 | $result .= '' . self::EOL; 180 | 181 | $content = ''; 182 | $lastUpdate = 0; 183 | 184 | foreach ($this->_items as $item) { 185 | $item['updated'] = $item['updated'] > 0 ? $item['updated'] : $item['published']; 186 | 187 | $content .= '' . self::EOL; 188 | $content .= '' . $this->encode($item['title']) . '' . self::EOL; 189 | $content .= '' . self::EOL; 190 | $content .= '' . (isset($item['id']) ? $item['id'] : $item['link']) . '' . self::EOL; 191 | $content .= '' . date('c', $item['updated']) . '' . self::EOL; 192 | $content .= '' . date('c', $item['published']) . '' . self::EOL; 193 | 194 | if (!empty($item['author'])) { 195 | $content .= ' 196 | ' . $item['author']['name'] . ' 197 | ' . $item['author']['url'] . ' 198 | ' . self::EOL; 199 | } 200 | 201 | if (!empty($item['category']) && is_array($item['category'])) { 202 | foreach ($item['category'] as $category) { 203 | $content .= '' . self::EOL; 204 | } 205 | } 206 | 207 | if (!empty($item['content'])) { 208 | $content .= '' . $this->encode($item['content']) . '' . self::EOL; 209 | } 210 | 211 | $content .= '' . self::EOL; 212 | 213 | if ($item['updated'] > $lastUpdate) { 214 | $lastUpdate = $item['updated']; 215 | } 216 | } 217 | 218 | $result .= '' . $this->encode($this->_title) . ' 219 | ' . $this->encode($this->_subTitle) . ' 220 | ' . date('c', $lastUpdate) . ' 221 | 222 | 223 | ' . $this->_feedUrl . ' 224 | http://www.creativecommons.org/licenses/by-sa/2.5/rdf 225 | ' . $content . ''; 226 | 227 | return $result; 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /workflow/import.php: -------------------------------------------------------------------------------- 1 | getMessage()); 24 | continue; 25 | } 26 | } 27 | 28 | // check username and password 29 | while (true) { 30 | $xmlrpc = new IXR_Client($url); 31 | $methods = $xmlrpc->query('system.listMethods'); 32 | 33 | $username = readline('Username: '); 34 | $password = readline('Password: '); 35 | 36 | if (!in_array('metaWeblog.getRecentPosts', $methods)) { 37 | le_fatal('method "%" not found on your server', 'metaWeblog.getRecentPosts'); 38 | } 39 | 40 | try { 41 | $xmlrpc->query('metaWeblog.getRecentPosts', 1, $username, $password, 1); 42 | break; 43 | } catch (Exception $e) { 44 | le_console('error', $e->getMessage()); 45 | continue; 46 | } 47 | } 48 | 49 | return [$xmlrpc, $username, $password, $methods]; 50 | }); 51 | 52 | // detect xmlrpc 53 | le_add_workflow('detect_xmlrpc_url', function ($url) { 54 | $ch = curl_init(); 55 | 56 | curl_setopt_array($ch, [ 57 | CURLOPT_URL => $url, 58 | CURLOPT_RETURNTRANSFER => true, 59 | CURLOPT_HEADER => false, 60 | CURLOPT_SSL_VERIFYPEER => false, 61 | CURLOPT_SSL_VERIFYHOST => false, 62 | CURLOPT_TIMEOUT => 20, 63 | CURLOPT_FOLLOWLOCATION => true, 64 | CURLOPT_MAXREDIRS => 3 65 | ]); 66 | 67 | $reponse = curl_exec($ch); 68 | $type = trim(explode(';', curl_getinfo($ch, CURLINFO_CONTENT_TYPE))[0]); 69 | $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); 70 | curl_close($ch); 71 | 72 | if (!$reponse || 200 != $status) { 73 | le_fatal('http server is not available: %s', $url); 74 | } 75 | 76 | if ('text/html' == $type) { 77 | $dom = new DOMDocument(); 78 | @$dom->loadHTML('' . $reponse); 79 | $xpath = new DOMXPath($dom); 80 | 81 | $items = $xpath->query('//link[@rel="EditURI"]'); 82 | if ($items->length > 0) { 83 | $href = $items->item(0)->getAttributeNode('href')->nodeValue; 84 | if (!empty($href)) { 85 | $rsd = new DOMDocument('1.0', 'UTF-8'); 86 | $rsd->load($href); 87 | $apis = $rsd->getElementsByTagName('api'); 88 | 89 | if ($apis->length > 0) { 90 | return $apis->item(0)->getAttributeNode('apiLink')->nodeValue; 91 | } 92 | } 93 | } 94 | } else if ('text/xml' == $type) { 95 | return $url; 96 | } 97 | 98 | le_fatal('no xmlrpc provider founded'); 99 | }); 100 | 101 | // import 102 | le_add_workflow('import', function ($xmlrpc, $username, $password, $methods) use ($context) { 103 | $wxrFile = tempnam(sys_get_temp_dir(), 'le'); 104 | $wxr = fopen($wxrFile, 'wb'); 105 | 106 | // write begin 107 | fwrite($wxr, ' 108 | 114 | '); 115 | 116 | $blogId = 1; 117 | if (in_array('metaWeblog.getUsersBlogs', $methods)) { 118 | le_console('info', 'fetching blog info'); 119 | $blogs = $xmlrpc->query('metaWeblog.getUsersBlogs', 1, $username, $password, 100); 120 | if (empty($blogs)) { 121 | le_fatal('user has no available blog'); 122 | } 123 | 124 | $blog = array_shift($blogs); 125 | $blogId = $blog['blogid']; 126 | } 127 | 128 | if (in_array('wp.getOptions', $methods)) { 129 | $options = $xmlrpc->query('wp.getOptions', $blogId, $username, $password); 130 | $context->config['globals']['url'] = $options['blog_url']['value']; 131 | $context->config['globals']['title'] = $options['blog_title']['value']; 132 | } 133 | 134 | $context->config['blocks']['category']['source'] = []; 135 | if (in_array('metaWeblog.getCategories', $methods)) { 136 | le_console('info', 'fetching categories'); 137 | $categories = $xmlrpc->query('metaWeblog.getCategories', $blogId, $username, $password); 138 | $context->config['blocks']['category']['source'] = []; 139 | 140 | foreach ($categories as $category) { 141 | $key = explode('.', basename(rtrim($category['htmlUrl'], '/')))[0]; 142 | $context->config['blocks']['category']['source'][$key] = $category['categoryName']; 143 | } 144 | } 145 | 146 | if (!file_put_contents($context->dir . 'config.yaml', Spyc::YAMLDump($context->config, 4))) { 147 | le_fatal('can not write to config file: %sconfig.yaml', $context->dir); 148 | } 149 | 150 | if (in_array('metaWeblog.getRecentPosts', $methods)) { 151 | le_console('info', 'fetching posts'); 152 | 153 | $posts = $xmlrpc->query('metaWeblog.getRecentPosts', $blogId, $username, $password, 1000); 154 | $source = $context->dir . $context->config['blocks']['post']['source']; 155 | $target = rtrim($context->config['globals']['url'], '/') . '/' 156 | . trim(isset($context->config['blocks']['post']['target']) ? $context->config['blocks']['post']['target'] : 'post', '/') 157 | . '/%s.' . (isset($context->config['blocks']['post']['ext']) ? $context->config['blocks']['post']['ext'] : 'html'); 158 | 159 | if (!is_dir($source)) { 160 | if (!mkdir($source, 0755, true)) { 161 | le_fatal('can not make post target directory: %s', $source); 162 | } 163 | } 164 | 165 | foreach ($posts as $post) { 166 | if ('publish' != $post['post_status']) { 167 | continue; 168 | } 169 | 170 | le_console('info', 'add %s', $post['wp_slug']); 171 | $content = le_do_workflow('filter_post', $post, $context->config['blocks']['category']['source']); 172 | file_put_contents($source . '/' . $post['wp_slug'] . '.md', $content); 173 | 174 | if (in_array('wp.getComments', $methods)) { 175 | $offset = 0; 176 | le_console('info', 'fetching comments: %s', $post['postid']); 177 | 178 | do { 179 | $comments = $xmlrpc->query('wp.getComments', $blogId, $username, $password, [ 180 | 'post_id' => $post['postid'], 181 | 'number' => 100, 182 | 'offset' => $offset 183 | ]); 184 | 185 | foreach ($comments as $c) { 186 | if (isset($c['type']) && 'comment' != $c['type']) { 187 | continue; 188 | } 189 | 190 | fwrite($wxr, " 191 | {$post['title']} 192 | " . sprintf($target, $post['wp_slug']) . " 193 | post:{$post['wp_slug']} 194 | " . date('Y-m-d H:i:s', $post['dateCreated']->getTimestamp()) . " 195 | open 196 | 197 | {$c['comment_id']} 198 | {$c['author']} 199 | {$c['author_email']} 200 | {$c['author_url']} 201 | {$c['author_ip']} 202 | " . date('Y-m-d H:i:s', $c['date_created_gmt']->getTimestamp()) . " 203 | 204 | " . ('approve' == $c['status'] ? 1 : 0) . " 205 | {$c['parent']} 206 | 207 | "); 208 | } 209 | 210 | $offset += 100; 211 | } while (count($comments) == 100); 212 | } 213 | } 214 | } 215 | 216 | fwrite($wxr, ' 217 | '); 218 | fclose($wxr); 219 | if (rename($wxrFile, $context->dir . '/wxr.xml')) { 220 | le_console('done', 'your comments WXR XML file has exported to %swxr.xml', $context->dir); 221 | } 222 | 223 | }); 224 | 225 | // filter post 226 | le_add_workflow('filter_post', function ($post, $categoriesConfig) { 227 | $text = (isset($post['description']) ? $post['description'] : '') 228 | . (isset($post['mt_text_more']) ? "\n\n\n\n" . $post['mt_text_more'] : ''); 229 | 230 | $text = preg_replace("/<\/p>\s*<\/p>/is", "

", $text); 231 | $text = preg_replace_callback("/]*>]*>(.+?)<\/code><\/pre>/is", function ($matches) { 232 | return '
' . $matches[1] . '
'; 233 | }, $text); 234 | $text = preg_replace_callback("/]*>(.+?)<\/code>/is", function ($matches) { 235 | if (false !== strpos($matches[1], "\n")) { 236 | return '
' . $matches[1] . '
'; 237 | } 238 | 239 | return '' . $matches[1] . ''; 240 | }, $text); 241 | $text = preg_replace("/]+(href=\"[^\"]+\")[^>]*>/is", "", $text); 242 | $text = preg_replace("/]+(src=\"[^\"]+\")[^>]*\/?>/is", "", $text); 243 | $parser = new \Markdownify\ConverterExtra(); 244 | 245 | $content = "\n" . $post['title'] . "\n" 246 | . str_repeat('=', strlen($post['title'])) . "\n\n" 247 | . $parser->parseString($text); 248 | 249 | $date = $post['dateCreated']; 250 | if (!empty($post['mt_keywords'])) { 251 | $content = "@tag:{$post['mt_keywords']}\n" . $content; 252 | } 253 | 254 | if (!empty($post['categories'])) { 255 | $categories = []; 256 | foreach ($post['categories'] as $category) { 257 | $found = array_search($category, $categoriesConfig); 258 | if (false !== $found) { 259 | $categories[] = $found; 260 | } 261 | } 262 | 263 | if (!empty($categories)) { 264 | $content = "@category:" . implode(',', $categories) . "\n" . $content; 265 | } 266 | } 267 | 268 | $content = "@date:{$date->year}-{$date->month}-{$date->day} {$date->hour}:{$date->minute}:{$date->second}\n" 269 | . $content; 270 | $content = "@slug:{$post['wp_slug']}\n" . $content; 271 | 272 | return $content; 273 | }); 274 | -------------------------------------------------------------------------------- /workflow/main.php: -------------------------------------------------------------------------------- 1 | 'Create an empty Logecho directory', 23 | 'build' => 'Build contents to _target directory', 24 | 'sync' => 'Sync _target by using your sync config', 25 | 'serve' => 'Start a http server to watch your site', 26 | 'watch' => '', 27 | 'archive' => '', 28 | 'help' => 'Show help documents', 29 | 'import' => 'Import data from other blogging platform which is using xmlrpc' 30 | ]; 31 | 32 | if (count($argv) > 0 && $argv[0] == $_SERVER['PHP_SELF']) { 33 | array_shift($argv); 34 | } 35 | 36 | if (count($argv) == 0) { 37 | $argv[] = 'help'; 38 | } 39 | 40 | $help = function () use ($opts) { 41 | echo "usage: logecho \n\n"; 42 | echo "Here are the most commonly used logecho commands:\n"; 43 | 44 | foreach ($opts as $name => $words) { 45 | $name = str_pad($name, 12, ' ', STR_PAD_RIGHT); 46 | echo " {$name}{$words}\n"; 47 | } 48 | }; 49 | 50 | $name = array_shift($argv); 51 | if (!isset($opts[$name])) { 52 | le_console('error', 'can not handle %s command, please use the following commands', $name); 53 | $help(); 54 | exit(1); 55 | } 56 | 57 | if ('help' == $name) { 58 | $help(); 59 | } else { 60 | if ($name != 'update') { 61 | if (count($argv) < 1) { 62 | le_fatal('a blog directory is required'); 63 | } 64 | 65 | list ($dir) = $argv; 66 | if (!is_dir($dir)) { 67 | le_fatal('blog directory "%s" is not exists', $dir); 68 | } 69 | 70 | $context->dir = rtrim($dir, '/') . '/'; 71 | $context->cmd = __DEBUG__ ? $_SERVER['_'] . ' ' . $_SERVER['PHP_SELF'] : $_SERVER['PHP_SELF']; 72 | 73 | if ($name != 'init') { 74 | le_do_workflow('read_config'); 75 | } 76 | } 77 | 78 | array_unshift($argv, $name); 79 | call_user_func_array('le_do_workflow', $argv); 80 | 81 | le_console('done', $name); 82 | } 83 | }); 84 | 85 | // read config 86 | le_add_workflow('read_config', function () use ($context) { 87 | $file = $context->dir . 'config.yaml'; 88 | if (!file_exists($file)) { 89 | le_fatal('can not find config file "%s"', $file); 90 | } 91 | 92 | $config = Spyc::YAMLLoad($file); 93 | if (!$config) { 94 | le_fatal('config file is not a valid yaml file'); 95 | } 96 | 97 | $context->config = $config; 98 | }); 99 | 100 | // sync directory 101 | le_add_workflow('sync', function ($source = null, $target = null) use ($context) { 102 | $url = $context->config['sync']; 103 | if (empty($url)) { 104 | le_fatal('Missing sync url configure'); 105 | } 106 | 107 | le_do_workflow('build'); 108 | $source = $context->dir . '_target'; 109 | $img = tempnam(sys_get_temp_dir(), 'le'); 110 | $data = ''; 111 | 112 | // compress all files 113 | $files = le_get_all_files($source); 114 | $offset = strlen($source); 115 | $first = true; 116 | 117 | foreach ($files as $file => $path) { 118 | if ($file[0] == '.') { 119 | continue; 120 | } 121 | 122 | $original = substr($path, $offset); 123 | $data .= ($first ? '' : "\n") . $original . ' ' . base64_encode(file_get_contents($path)); 124 | $first = false; 125 | } 126 | 127 | $ch = curl_init(); 128 | 129 | curl_setopt_array($ch, [ 130 | CURLOPT_URL => $url, 131 | CURLOPT_RETURNTRANSFER => true, 132 | CURLOPT_HEADER => false, 133 | CURLOPT_SSL_VERIFYPEER => false, 134 | CURLOPT_SSL_VERIFYHOST => false, 135 | CURLOPT_TIMEOUT => 20, 136 | CURLOPT_POST => true, 137 | CURLOPT_POSTFIELDS => $data 138 | ]); 139 | 140 | $response = curl_exec($ch); 141 | if (false === $response) { 142 | le_fatal(curl_error($ch)); 143 | } 144 | 145 | $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); 146 | if (200 != $code) { 147 | le_fatal($code); 148 | } 149 | }); 150 | 151 | // build all 152 | le_add_workflow('build', function () use ($context) { 153 | le_do_workflow('compile.init'); 154 | le_do_workflow('compile.compile'); 155 | 156 | $source = $context->dir . '_public'; 157 | $target = $context->dir . '_target/public'; 158 | 159 | // delete all files in target 160 | $files = le_get_all_files($target); 161 | $dirs = []; 162 | 163 | foreach ($files as $file => $path) { 164 | $dir = dirname($path); 165 | 166 | // do not remove root directory 167 | if (!in_array($dir, $dirs) && realpath($dir) != realpath($target)) { 168 | $dirs[] = $dir; 169 | } 170 | 171 | // remove all files first 172 | if (!unlink($path)) { 173 | le_fatal('can not unlink file %s, permission denied', $path); 174 | } 175 | } 176 | 177 | // remove all dirs 178 | $dirs = array_reverse($dirs); 179 | foreach ($dirs as $dir) { 180 | if (!rmdir($dir)) { 181 | le_fatal('can not rm directory %s, permission denied', $dir); 182 | } 183 | } 184 | 185 | // copy all files 186 | $files = le_get_all_files($source); 187 | $offset = strlen($source); 188 | 189 | foreach ($files as $file => $path) { 190 | if ($file[0] == '.') { 191 | continue; 192 | } 193 | 194 | $original = substr($path, $offset); 195 | $current = $target . '/' . $original; 196 | $dir = dirname($current); 197 | 198 | if (!is_dir($dir)) { 199 | if (!mkdir($dir, 0755, true)) { 200 | le_fatal('can not make directory %s, permission denied', $dir); 201 | } 202 | } 203 | 204 | copy($path, $current); 205 | } 206 | }); 207 | 208 | // init 209 | le_add_workflow('init', function () use ($context) { 210 | if (file_exists($context->dir . 'config.yaml')) { 211 | $confirm = readline('target dir is not empty, continue? (Y/n) '); 212 | 213 | if (strtolower($confirm) != 'y') { 214 | exit; 215 | } 216 | } 217 | 218 | $dir = __DIR__ . '/../sample'; 219 | $offset = strlen($dir); 220 | 221 | $files = le_get_all_files($dir); 222 | 223 | foreach ($files as $file => $path) { 224 | if ($file[0] == '.') { 225 | continue; 226 | } 227 | 228 | $original = substr($path, $offset); 229 | $target = $context->dir . $original; 230 | $dir = dirname($target); 231 | 232 | if (!is_dir($dir)) { 233 | if (!mkdir($dir, 0755, true)) { 234 | le_fatal('can not make directory %s, permission denied', $dir); 235 | } 236 | } 237 | 238 | copy($path, $target); 239 | } 240 | }); 241 | 242 | // serve 243 | le_add_workflow('serve', function () use ($context) { 244 | $target = $context->dir . '_target'; 245 | if (!is_dir($target)) { 246 | le_console('info', 'building target files, please wait ...'); 247 | exec($context->cmd . ' build ' . $context->dir); 248 | } 249 | 250 | $proc = proc_open($context->cmd . ' watch ' . $context->dir, [ 251 | 0 => ['pipe', 'r'], 252 | 1 => ['pipe', 'w'], 253 | 2 => ['file', sys_get_temp_dir() . '/logecho-error.log', 'a'] 254 | ], $pipes, getcwd()); 255 | stream_set_blocking($pipes[0], 0); 256 | stream_set_blocking($pipes[1], 0); 257 | 258 | le_console('info', 'Listening on localhost:7000'); 259 | le_console('info', 'Document root is %s', $target); 260 | le_console('info', 'Press Ctrl-C to quit'); 261 | exec('/usr/bin/env php -S localhost:7000 -t ' . $target); 262 | }); 263 | 264 | // archive 265 | le_add_workflow('archive', function () use ($context) { 266 | // init complier 267 | le_do_workflow('compile.init'); 268 | 269 | foreach ($context->config['blocks'] as $type => $block) { 270 | if (!isset($block['source']) || !is_string($block['source'])) { 271 | continue; 272 | } 273 | 274 | $source = trim($block['source'], '/'); 275 | $files = glob($context->dir . '/' . $source . '/*.md'); 276 | $list = []; 277 | 278 | foreach ($files as $file) { 279 | list ($metas) = le_do_workflow('compile.get_metas', $file); 280 | $date = $metas['date']; 281 | 282 | $list[$file] = $date; 283 | } 284 | 285 | asort($list); 286 | $index = 1; 287 | 288 | foreach ($list as $file => $date) { 289 | $info = pathinfo($file); 290 | $fileName = $info['filename']; 291 | $dir = $info['dirname']; 292 | 293 | if (preg_match("/^[0-9]{4}\.(.+)$/", $fileName, $matches)) { 294 | $fileName = $matches[1]; 295 | } 296 | 297 | $source = realpath($file); 298 | $target = rtrim($dir, '/') . '/' . str_pad($index, 4, '0', STR_PAD_LEFT) . '.' . $fileName . '.md'; 299 | 300 | if ($source != $target && !file_exists($target)) { 301 | le_console('info', basename($source) . ' => ' . basename($target)); 302 | rename($source, $target); 303 | } 304 | 305 | $index ++; 306 | } 307 | } 308 | }); 309 | 310 | // watch 311 | le_add_workflow('watch', function () use ($context) { 312 | $lastSum = ''; 313 | 314 | while (true) { 315 | // get sources 316 | $sources = ["\/_theme\/", "\/_public\/"]; 317 | $sum = ''; 318 | 319 | foreach ($context->config['blocks'] as $type => $block) { 320 | if (!isset($block['source']) || !is_string($block['source'])) { 321 | continue; 322 | } 323 | 324 | $source = trim($block['source'], '/'); 325 | $source = empty($source) ? '/' : '/' . $source . '/'; 326 | 327 | $sources[] = preg_quote($source, '/'); 328 | } 329 | 330 | if (!empty($sources)) { 331 | $regex = "/^" . preg_quote(rtrim($context->dir, '/')) 332 | . "(" . implode('|', $sources) . ")/"; 333 | 334 | $files = le_get_all_files($context->dir); 335 | 336 | foreach ($files as $file => $path) { 337 | if (!preg_match($regex, $path) || $file[0] == '.') { 338 | continue; 339 | } 340 | 341 | $sum .= md5_file($path); 342 | } 343 | 344 | $sum = md5($sum . md5_file($context->dir . 'config.yaml')); 345 | if ($lastSum != $sum) { 346 | exec($context->cmd . ' build ' . $context->dir); 347 | $lastSum = $sum; 348 | } 349 | } 350 | 351 | sleep(1); 352 | } 353 | }); 354 | 355 | // import 356 | le_add_workflow('import', function () use ($context) { 357 | le_do_workflow('import.init'); 358 | }); 359 | -------------------------------------------------------------------------------- /workflow/compile.php: -------------------------------------------------------------------------------- 1 | indexConfig = []; 8 | $context->metas = []; 9 | $context->data = []; 10 | $context->index = []; 11 | $context->cached = []; 12 | $context->sitemap = []; 13 | 14 | if (isset($context->config['blocks']['index'])) { 15 | $context->indexConfig = $context->config['blocks']['index']; 16 | unset($context->config['blocks']['index']); 17 | } 18 | 19 | // init twig 20 | $loader = new Twig_Loader_Filesystem($context->dir . '_theme'); 21 | $context->template = new Twig_Environment($loader, [ 22 | 'autoescape' => false 23 | ]); 24 | 25 | le_do_workflow('init_twig', $context->template); 26 | 27 | // read config 28 | le_do_workflow('read_metas'); 29 | le_do_workflow('read_globals'); 30 | }); 31 | 32 | // load extension 33 | le_add_workflow('init_twig', function (Twig_Environment $twig) use ($context) { 34 | $twig->addFilter(new Twig_SimpleFilter('more', function ($str, $limit = 0) { 35 | if ($limit > 0) { 36 | $str = strip_tags($str); 37 | return mb_strlen($str, 'UTF-8') > $limit 38 | ? mb_substr($str, 0, $limit, 'UTF-8') . ' ...' : $str; 39 | } 40 | 41 | $parts = preg_split("//is", $str); 42 | return count($parts) > 1 ? $parts[0] . '

...

' : $str; 43 | })); 44 | }); 45 | 46 | // read metas 47 | le_add_workflow('read_metas', function () use ($context) { 48 | // read metas from post 49 | foreach ($context->config['blocks'] as $type => $block) { 50 | if (isset($block['source']) && is_string($block['source'])) { 51 | $files = glob($context->dir . $block['source'] . '/*.md'); 52 | 53 | foreach ($files as $file) { 54 | list ($metas) = le_do_workflow('get_metas', $file); 55 | $term = pathinfo($file, PATHINFO_FILENAME); 56 | 57 | $context->index[$type][$term] = $metas['date']; 58 | 59 | // get metas group from file 60 | foreach ($metas as $key => $relates) { 61 | if (!is_array($relates)) { 62 | continue; 63 | } 64 | 65 | // link meta and post 66 | foreach ($relates as $relate) { 67 | $context->metas[$key][$relate][] = [$type, $term]; 68 | } 69 | } 70 | } 71 | } 72 | } 73 | 74 | $context->index = array_map(function ($index) { 75 | arsort($index); 76 | return array_keys($index); 77 | }, $context->index); 78 | 79 | if (!empty($context->metas['archive'])) { 80 | krsort($context->metas['archive']); 81 | } 82 | 83 | foreach ($context->metas as $type => $relates) { 84 | foreach ($relates as $key => $relate) { 85 | usort($relate, function ($a, $b) use ($context) { 86 | $x = array_search($a[1], $context->index[$a[0]]); 87 | $y = array_search($b[1], $context->index[$b[0]]); 88 | 89 | return $x > $y ? 1 : -1; 90 | }); 91 | 92 | $context->metas[$type][$key] = $relate; 93 | } 94 | } 95 | }); 96 | 97 | // read globals 98 | le_add_workflow('read_globals', function () use ($context) { 99 | if (isset($context->config['globals'])) { 100 | $context->data = $context->config['globals']; 101 | } 102 | 103 | $context->data['metas'] = []; 104 | foreach ($context->config['blocks'] as $key => $val) { 105 | if (isset($val['source']) && is_string($val['source'])) { 106 | continue; 107 | } 108 | 109 | $context->data['metas'][$key] = []; 110 | 111 | $path = '/' . trim($val['target'], '/'); 112 | $url = '/' == substr($val['target'], -1) 113 | ? $path . '/%s.' . (isset($val['ext']) ? $val['ext'] : 'html') : $path . '#' . $key . '-%s'; 114 | 115 | if (isset($val['source']) && is_array($val['source'])) { 116 | foreach ($val['source'] as $slug => $name) { 117 | $context->data['metas'][$key][$slug] = [ 118 | 'slug' => $slug, 119 | 'name' => $name, 120 | 'url' => sprintf($url, urlencode($slug)), 121 | 'count' => isset($context->metas[$key][$slug]) ? count($context->metas[$key][$slug]) : 0 122 | ]; 123 | } 124 | } else if (!isset($val['source']) && !empty($context->metas[$key])) { 125 | foreach ($context->metas[$key] as $slug => $terms) { 126 | $context->data['metas'][$key][$slug] = [ 127 | 'slug' => $slug, 128 | 'name' => $slug, 129 | 'url' => sprintf($url, urlencode($slug)), 130 | 'count' => count($terms) 131 | ]; 132 | } 133 | } 134 | } 135 | 136 | foreach ($context->data as $key => $val) { 137 | $context->template->addGlobal($key, $val); 138 | } 139 | }); 140 | 141 | // get metas 142 | le_add_workflow('get_metas', function ($file) use ($context) { 143 | $str = ltrim(file_get_contents($file)); 144 | $metas = []; 145 | 146 | $lines = explode("\n", $str); 147 | foreach ($lines as $index => $line) { 148 | if (preg_match("/^@([_a-z0-9-]+):(.+)$/i", $line, $matches)) { 149 | $key = strtolower($matches[1]); 150 | if ('date' == $key) { 151 | $metas['date'] = strtotime(trim($matches[2])); 152 | continue; 153 | } else if (!isset($context->config['blocks'][$key])) { 154 | $metas[$key] = trim($matches[2]); 155 | continue; 156 | } 157 | 158 | // read block 159 | $block = $context->config['blocks'][$key]; 160 | $values = array_map('trim', explode(',', trim($matches[2]))); 161 | 162 | foreach ($values as $value) { 163 | if (isset($block['source'])) { 164 | if (is_array($block['source'])) { 165 | // specific by hash object 166 | if (isset($block['source'][$value])) { 167 | $metas[$key][] = $value; 168 | } else if (in_array($value, $block['source'])) { 169 | $metas[$key][] = array_search($value, $block['source']); 170 | } 171 | } else { 172 | $file = $context->dir . $block['source'] . '/' . $value . '.md'; 173 | if (file_exists($file)) { 174 | $metas[$key][] = $value; 175 | } 176 | } 177 | } else { 178 | $metas[$key][] = $value; 179 | } 180 | } 181 | } else { 182 | break; 183 | } 184 | } 185 | 186 | if (!isset($metas['date'])) { 187 | $metas['date'] = filemtime($file); 188 | } 189 | 190 | // special archive block 191 | if (!isset($metas['archive']) && isset($context->config['blocks']['archive'])) { 192 | $metas['archive'][] = date('Y.m', $metas['date']); 193 | } 194 | 195 | $str = implode("\n", array_slice($lines, $index)); 196 | return [$metas, $str]; 197 | }); 198 | 199 | // parse 200 | le_add_workflow('parse', function ($text) use ($context) { 201 | static $handler; 202 | 203 | if (empty($handler)) { 204 | $parser = isset($context->parser) ? $context->parser : 'markdown'; 205 | $defaults = [ 206 | 'markdown' => '\Michelf\MarkdownExtra#defaultTransform', 207 | 'parsedown' => 'ParsedownExtra#text' 208 | ]; 209 | 210 | $parser = isset($defaults[$parser]) ? $defaults[$parser] : $parser; 211 | 212 | if (strpos($parser, ':')) { 213 | $parts = explode(':', $parser, 2); 214 | list ($file, $parser) = $parts; 215 | require_once $file; 216 | } 217 | 218 | $method = 'text'; 219 | if (strpos($parser, '#')) { 220 | list ($className, $method) = explode('#', $parser); 221 | } else { 222 | $className = $parser; 223 | } 224 | 225 | if (!class_exists($className)) { 226 | le_fatal('can not find parser class "%s"', $className); 227 | } 228 | 229 | $ref = new ReflectionClass($className); 230 | $methodRef = $ref->getMethod($method); 231 | 232 | if (empty($methodRef)) { 233 | le_fatal('can not find method "%s:%s"', $className, $method); 234 | } 235 | 236 | $handler = [$methodRef->isStatic() ? $className : $ref->newInstance(), $method]; 237 | } 238 | 239 | return call_user_func($handler, $text); 240 | }); 241 | 242 | // get post 243 | le_add_workflow('get_post', function ($type, $key) use ($context) { 244 | $block = $context->config['blocks'][$type]; 245 | $file = $context->dir . $block['source'] . '/' . $key . '.md'; 246 | list ($result, $text) = le_do_workflow('get_metas', $file); 247 | $base = !empty($context->data['url']) ? rtrim($context->data['url'], '/') : ''; 248 | 249 | // expand metas 250 | foreach ($result as $name => &$metas) { 251 | if (!is_array($metas)) { 252 | continue; 253 | } 254 | 255 | if (isset($context->data['metas'][$name])) { 256 | $current = $context->data['metas'][$name]; 257 | $metas = array_map(function ($index) use ($current) { 258 | return $current[$index]; 259 | }, $metas); 260 | } 261 | } 262 | 263 | $result['type'] = $type; 264 | $result['id'] = $type . ':' . $key; 265 | $result['title'] = $key; 266 | $result['text'] = $text; 267 | $result['content'] = le_do_workflow('parse', $text); 268 | $result['ext'] = isset($block['ext']) ? $block['ext'] : 'html'; 269 | if (!isset($result['slug'])) { 270 | $result['slug'] = preg_match("/^[0-9]{4}\.(.+)$/", $key, $matches) ? $matches[1] : $key; 271 | } 272 | $result['url'] = '/' . trim($block['target'], '/') 273 | . '/' . urlencode($result['slug']) . '.' . $result['ext']; 274 | $result['permalink'] = $base . $result['url']; 275 | 276 | $dom = new \DOMDocument(); 277 | @$dom->loadHTML('' . 278 | '' 279 | . $result['content'] . ''); 280 | $xpath = new \DOMXPath($dom); 281 | 282 | $items = $xpath->query('//h1|//h2'); 283 | 284 | if ($items->length > 0) { 285 | $result['title'] = str_replace(["\n", "\r", "\t"], '', $items->item(0)->nodeValue); 286 | $items->item(0)->parentNode->removeChild($items->item(0)); 287 | $html = $dom->saveHTML($dom->documentElement); 288 | $start = strpos($html, ''); 289 | $stop = strrpos($html, ''); 290 | $result['content'] = substr($html, $start + 6, $stop - $start - 6); 291 | } 292 | 293 | return $result; 294 | }); 295 | 296 | // get post context 297 | le_add_workflow('get_post_context', function ($type, $key) use ($context) { 298 | $index = array_search($key, $context->index[$type]); 299 | $result = [ 300 | 'prev' => NULL, 301 | 'next' => NULL 302 | ]; 303 | 304 | if ($index > 0) { 305 | $result['prev'] = le_do_workflow('get_post', $type, $context->index[$type][$index - 1]); 306 | } 307 | 308 | if ($index < (count($context->index[$type]) - 1)) { 309 | $result['next'] = le_do_workflow('get_post', $type, $context->index[$type][$index + 1]); 310 | } 311 | 312 | return $result; 313 | }); 314 | 315 | // get meta posts 316 | le_add_workflow('get_meta_posts', function ($type, $key) use ($context) { 317 | $result = []; 318 | 319 | if (isset($context->metas[$type][$key])) { 320 | foreach ($context->metas[$type][$key] as $val) { 321 | list ($postType, $postKey) = $val; 322 | 323 | $index = $postType . ':' . $postKey; 324 | if (!isset($context->cached[$index])) { 325 | $context->cached[$index] = le_do_workflow('get_post', $postType, $postKey); 326 | unset($context->cached[$index]['content']); 327 | } 328 | 329 | $result[$postType][] = $context->cached[$index]; 330 | } 331 | } 332 | 333 | foreach ($result as &$archive) { 334 | usort($archive, function ($a, $b) use ($context) { 335 | list ($aType, $aId) = explode(':', $a['id']); 336 | list ($bType, $bId) = explode(':', $b['id']); 337 | 338 | $x = array_search($aId, $context->index[$aType]); 339 | $y = array_search($bId, $context->index[$bType]); 340 | 341 | return $x > $y ? 1 : -1; 342 | }); 343 | } 344 | 345 | return $result; 346 | }); 347 | 348 | // build 349 | le_add_workflow('build', function ($template, $file, $data = []) use ($context) { 350 | $html = $context->template->render($template, $data); 351 | 352 | $file = $context->dir . '/_target/' . $file; 353 | $dir = dirname($file); 354 | 355 | if (!is_dir($dir)) { 356 | if (!mkdir($dir, 0755, true)) { 357 | le_fatal('directory is not exists "%s"', $dir); 358 | } 359 | } 360 | 361 | file_put_contents($file, $html); 362 | }); 363 | 364 | // compile post 365 | le_add_workflow('compile_post', function ($type, $specific = NULL) use ($context) { 366 | $block = $context->config['blocks'][$type]; 367 | 368 | if (!isset($block['source']) || !is_string($block['source']) || empty($block['target'])) { 369 | le_fatal('block is not exists "%s"', $type); 370 | } 371 | 372 | $files = glob($context->dir . $block['source'] . '/*.md'); 373 | $template = $type. '.twig'; 374 | $target = $block['target'] . '/'; 375 | 376 | if (isset($block['template'])) { 377 | $template = $block['template']; 378 | } 379 | 380 | foreach ($files as $file) { 381 | le_console('info', 'compile %s', preg_replace("/\/+/", '/', $file)); 382 | 383 | $key = pathinfo($file, PATHINFO_FILENAME); 384 | 385 | if (!empty($specific) && $key != $specific) { 386 | continue; 387 | } 388 | 389 | $post = array_merge(le_do_workflow('get_post', $type, $key), 390 | le_do_workflow('get_post_context', $type, $key)); 391 | $currentTemplate = isset($post['template']) ? $post['template'] : $template; 392 | 393 | // add sitemap 394 | $context->sitemap[$post['url']] = 0.64; 395 | 396 | le_do_workflow('build', $currentTemplate, $target . $post['slug'] . '.' . $post['ext'], [ 397 | $type => $post 398 | ]); 399 | } 400 | }); 401 | 402 | // compile posts 403 | le_add_workflow('compile_posts', function () use ($context) { 404 | foreach ($context->config['blocks'] as $type => $val) { 405 | if (isset($val['source']) && is_string($val['source'])) { 406 | le_do_workflow('compile_post', $type); 407 | } 408 | } 409 | }); 410 | 411 | // compile metas 412 | le_add_workflow('compile_metas', function () use ($context) { 413 | $targets = []; 414 | 415 | foreach ($context->config['blocks'] as $type => $val) { 416 | if (isset($val['source']) && is_string($val['source'])) { 417 | continue; 418 | } 419 | 420 | $target = isset($val['target']) ? $val['target'] : $type . '.html'; 421 | $template = isset($val['template']) ? $val['template'] : $type . '.twig'; 422 | 423 | if ('/' == substr($target, -1)) { 424 | if (!empty($context->data['metas'][$type])) { 425 | foreach ($context->data['metas'][$type] as $key => $meta) { 426 | $targets[$target . $key . '.html:' . $template][] = $type . ':' . $key; 427 | } 428 | } 429 | } else { 430 | $targets[$target . ':' . $template][] = $type; 431 | } 432 | } 433 | 434 | foreach ($targets as $define => $links) { 435 | $data = []; 436 | $links = array_unique($links); 437 | list ($target, $template) = explode(':', $define); 438 | 439 | foreach ($links as $link) { 440 | $parts = explode(':', $link); 441 | $type = $parts[0]; 442 | 443 | if (isset($parts[1])) { 444 | $data[$type] = array_merge($context->data['metas'][$type][$parts[1]], 445 | le_do_workflow('get_meta_posts', $type, $parts[1])); 446 | } else { 447 | if (!empty($context->data['metas'][$type])) { 448 | foreach ($context->data['metas'][$type] as $key => $val) { 449 | $data[$type][$key] = array_merge($val, le_do_workflow('get_meta_posts', $type, $key)); 450 | } 451 | } 452 | } 453 | } 454 | 455 | // add sitemap 456 | $context->sitemap[$target] = 0.8; 457 | le_do_workflow('build', $template, $target, $data); 458 | } 459 | }); 460 | 461 | // compile index 462 | le_add_workflow('compile_index', function () use ($context) { 463 | $index = []; 464 | 465 | if (empty($context->indexConfig)) { 466 | return; 467 | } 468 | 469 | $config = $context->indexConfig; 470 | $template = isset($config['template']) ? $config['template'] : 'index.twig'; 471 | $target = isset($config['target']) ? $config['target'] : 'index.html'; 472 | $limit = isset($config['limit']) ? $config['limit'] : 10; 473 | 474 | foreach ($context->config['blocks'] as $type => $val) { 475 | if (isset($val['source']) && is_string($val['source']) 476 | && isset($context->index[$type])) { 477 | if (is_array($limit)) { 478 | $currentLimit = isset($limit[$type]) ? $limit[$type] : 0; 479 | } else { 480 | $currentLimit = $limit; 481 | } 482 | 483 | if (0 == $currentLimit) { 484 | continue; 485 | } 486 | 487 | $posts = array_slice($context->index[$type], 0, $currentLimit); 488 | 489 | foreach ($posts as $post) { 490 | $index[$type][] = le_do_workflow('get_post', $type, $post); 491 | } 492 | } 493 | } 494 | 495 | // add sitemap 496 | $context->sitemap['/index.html'] = 1; 497 | 498 | le_do_workflow('build', $template, $target, [ 499 | 'index' => $index 500 | ]); 501 | }); 502 | 503 | // generate feeds 504 | le_add_workflow('generate_feeds', function () use ($context) { 505 | if (!isset($context->config['feeds'])) { 506 | return; 507 | } 508 | 509 | $config = $context->config['feeds']; 510 | 511 | if (!isset($config['source']) || !isset($context->index[$config['source']])) { 512 | return; 513 | } 514 | 515 | $config = array_merge([ 516 | 'title' => isset($context->data['title']) ? $context->data['title'] : 'My Feeds', 517 | 'description' => isset($context->data['description']) ? $context->data['description'] : 'My Feeds Description', 518 | 'recent' => 20, 519 | 'target' => 'feeds.xml', 520 | 'url' => isset($context->data['url']) ? $context->data['url'] : '/' 521 | ], $config); 522 | 523 | $feedsUrl = rtrim($config['url'], '/') . '/' . ltrim($config['target'], '/'); 524 | 525 | $feeds = new \Atom(); 526 | $feeds->setBaseUrl($config['url']); 527 | $feeds->setFeedUrl($feedsUrl); 528 | $feeds->setTitle($config['title']); 529 | $feeds->setSubTitle($config['description']); 530 | 531 | $posts = array_slice($context->index[$config['source']], 0, $config['recent']); 532 | foreach ($posts as $post) { 533 | $post = le_do_workflow('get_post', $config['source'], $post); 534 | $item = [ 535 | 'title' => $post['title'], 536 | 'link' => $post['permalink'], 537 | 'updated' => $post['date'], 538 | 'published' => $post['date'], 539 | 'author' => isset($config['author']) ? [ 540 | 'name' => $config['author'], 541 | 'url' => $config['url'] 542 | ] : NULL, 543 | 'content' => $post['content'] 544 | ]; 545 | 546 | foreach ($context->config['blocks'] as $type => $val) { 547 | if (isset($val['source']) && is_string($val['source'])) { 548 | continue; 549 | } 550 | 551 | if ('archive' != $type && !empty($post[$type])) { 552 | foreach ($post[$type] as $meta) { 553 | $item['category'][] = [ 554 | 'feeds_url' => $meta['url'], 555 | 'name' => $meta['name'] 556 | ]; 557 | } 558 | } 559 | } 560 | 561 | $feeds->addItem($item); 562 | } 563 | 564 | $target = $context->dir . '_target/' . $config['target']; 565 | $targetDir = dirname($target); 566 | 567 | if (!is_dir($targetDir)) { 568 | if (!mkdir($targetDir, 0755, true)) { 569 | le_fatal('feeds directory is not exists "%s"', $targetDir); 570 | } 571 | } 572 | 573 | file_put_contents($target, $feeds->generate()); 574 | }); 575 | 576 | // generate sitemap 577 | le_add_workflow('generate_sitemap', function () use ($context) { 578 | $fp = fopen($context->dir . '_target/sitemap.xml', 'wb'); 579 | if (!$fp) { 580 | le_fatal('can not write sitemap.xml'); 581 | } 582 | 583 | $base = isset($context->data['url']) ? rtrim($context->data['url'], '/') : '/'; 584 | 585 | fwrite($fp, ' 586 | '); 591 | 592 | foreach ($context->sitemap as $url => $priority) { 593 | $priority = number_format($priority, 2, '.', ''); 594 | $url = $base . '/' . ltrim($url, '/'); 595 | 596 | fwrite($fp, " 597 | 598 | {$url} 599 | daily 600 | $priority 601 | "); 602 | } 603 | 604 | fwrite($fp, ' 605 | '); 606 | fclose($fp); 607 | }); 608 | 609 | // compile 610 | le_add_workflow('compile', function () use ($context) { 611 | le_do_workflow('compile_index'); 612 | le_do_workflow('compile_metas'); 613 | le_do_workflow('compile_posts'); 614 | le_do_workflow('generate_feeds'); 615 | le_do_workflow('generate_sitemap'); 616 | }); 617 | --------------------------------------------------------------------------------