├── .gitignore ├── Context.sublime-menu ├── Default.sublime-commands ├── LICENSE ├── Main.sublime-menu ├── README.md ├── SublimeServer.py ├── favicon.ico ├── markdown.js └── style.example.css /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc -------------------------------------------------------------------------------- /Context.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { "command": "sublimeserver_browser", "caption": "View in SublimeServer" } 3 | ] -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "command": "sublimeserver_start", 4 | "caption": "SublimeServer: Start SublimeServer" 5 | }, 6 | { 7 | "command": "sublimeserver_stop", 8 | "caption": "SublimeServer: Stop SublimeServer" 9 | }, 10 | { 11 | "command": "sublimeserver_restart", 12 | "caption": "SublimeServer: Restart SublimeServer" 13 | }, 14 | { 15 | "caption": "SublimeServer: Manage SublimeServer Settings", 16 | "command": "open_file", 17 | "args": { 18 | "file": "${packages}/User/SublimeServer.sublime-settings" 19 | } 20 | } 21 | ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Yongning Liang 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Tools", 4 | "mnemonic": "T", 5 | "id": "tools", 6 | "children": 7 | [ 8 | { 9 | "caption": "SublimeServer", 10 | "children": 11 | [ 12 | { "command": "sublimeserver_start", "caption": "Start SublimeServer" }, 13 | { "command": "sublimeserver_stop", "caption": "Stop SublimeServer" }, 14 | { "command": "sublimeserver_restart", "caption": "Restart SublimeServer" }, 15 | { "caption": "-" }, 16 | { 17 | "caption": "Settings...", 18 | "command": "open_file", 19 | "args": { 20 | "file": "${packages}/User/SublimeServer.sublime-settings" 21 | } 22 | } 23 | ] 24 | } 25 | 26 | ] 27 | } 28 | ] 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SublimeServer 2 | 3 | #### Turn you Sublime Text editor into a HTTP server, and serves all the open project folders, now support ST2 and ST3 4 | 5 | ------ 6 | 7 | ## Markdown Rendering 8 | When a client requests a Markdown file(.md), the server will return an Html file instead of the requested Markdown file, the Html file contains all of content of the Markdown file and will render Markdown syntax into Html syntax on the client side. 9 | 10 | ### Setup 11 | Open Tools-SublimeServer-Settings, add mime types for Markdown like below 12 | 13 | { 14 | "attempts": 5, 15 | "autorun": false, 16 | "interval": 500, 17 | "mimetypes": 18 | { 19 | "": "application/octet-stream", 20 | ".c": "text/plain", 21 | ".h": "text/plain", 22 | ".markdown": "text/x-markdown; charset=UTF-8", 23 | ".md": "text/x-markdown; charset=UTF-8", 24 | ".py": "text/plain" 25 | }, 26 | "port": 8080 27 | } 28 | 29 | ------ 30 | 31 | ### Change Log 32 | 33 | #### 0.3.3 - Mar 6, 2016 34 | 35 | - Fix bug [#25](https://github.com/learning/SublimeServer/issues/25) 36 | - Add OPTIONS, PUT, POST support, thanks [fantonangeli](https://github.com/fantonangeli) 37 | 38 | #### 0.3.2 - Oct 12, 2014 39 | 40 | - Add markdown support, thanks rookiecj. 41 | - Add default stylesheet. 42 | - Add default extension setting, thanks [jdiamond](https://github.com/jdiamond). 43 | - Fix some ST3 problems 44 | 45 | #### 0.3.1 - Jun 1, 2014 46 | 47 | Add Sublime Text 3 support. 48 | 49 | #### 0.2.1 - Aug 31, 2012 50 | 51 | Improvements 52 | - Add auto-start support, Thanks [sapara](https://github.com/jdiamond).(#8) 53 | 54 | #### 0.2.0 - Jul 20, 2012 55 | 56 | Bug fix: 57 | 58 | While dragging new folders to Sublime or remove folders from Sublime, SublimeServer cannot refresh it.(#4) 59 | Improvements 60 | 61 | - Custom mime-types support. 62 | - Disable unavailable menu items, Thanks [bizoo](https://github.com/jdiamond).(#6) 63 | 64 | #### 0.1.2 - Jun 28, 2012 65 | 66 | Bug fix: 67 | - Thread still alive and cannot stop.(#2) 68 | 69 | Misc: 70 | - Move SublimeServer.sublime-settings to User folder. 71 | 72 | #### 0.1.0 - Jun 02, 2012 73 | 74 | SublimeServer can basically use 75 | 76 | Know issues: 77 | 78 | - While sublime reload plugins, last SublimeServer thread still alive and cannot stop it.(#2) 79 | - While dragging new folders to Sublime or remove folders from Sublime, SublimeServer cannot refresh it.(#4) 80 | 81 | 82 | ### For more details please visit [http://learning.github.com/SublimeServer](http://learning.github.com/SublimeServer) 83 | -------------------------------------------------------------------------------- /SublimeServer.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # SublimeServer 0.3.3 3 | # ------------------------------------------------------------------------------ 4 | __VERSION__ = "0.3.3" 5 | 6 | import os 7 | import sys 8 | import sublime 9 | import sublime_plugin 10 | import threading 11 | import webbrowser 12 | import posixpath 13 | import socket 14 | import cgi 15 | import shutil 16 | import mimetypes 17 | import time 18 | import io 19 | 20 | # detect python's version 21 | python_version = sys.version_info[0] 22 | 23 | # Sublime 3 (Python 3.x) 24 | if python_version == 3: 25 | from http.server import BaseHTTPRequestHandler, HTTPServer 26 | from socketserver import ThreadingMixIn, TCPServer 27 | # from io import StringIO 28 | from urllib import parse as urllib 29 | 30 | # Sublime 2 (Python 2.x) 31 | else: 32 | from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer 33 | from SocketServer import ThreadingMixIn, TCPServer 34 | # from StringIO import StringIO 35 | import urllib 36 | 37 | # SublimeServer Settings 38 | settings = None 39 | # HTTP server thread 40 | thread = None 41 | # Open directories 42 | dic = None 43 | # Fail attempts 44 | attempts = 0 45 | # Sublime complete loaded? 46 | loaded = False 47 | 48 | 49 | def load_settings(): 50 | '''Load SublimeServer Settings''' 51 | # default settings 52 | defaultPort = 8080 53 | # default attempts 54 | defaultAttempts = 5 55 | # default interval 56 | defaultInterval = 500 57 | # default mimeType 58 | defaultMimeTypes = { 59 | '': 'application/octet-stream', # Default 60 | '.py': 'text/plain', 61 | '.c': 'text/plain', 62 | '.h': 'text/plain', 63 | } 64 | # default autorun 65 | defaultAutorun = False 66 | # default extension 67 | defaultExtension = '.html' 68 | 69 | # load SublimeServer settings 70 | s = sublime.load_settings('SublimeServer.sublime-settings') 71 | 72 | # if setting file not exists, set to default 73 | if not s.has('port'): 74 | s.set('port', defaultPort) 75 | if not s.has('attempts'): 76 | s.set('attempts', defaultAttempts) 77 | if not s.has('interval'): 78 | s.set('interval', defaultInterval) 79 | if not s.has('mimetypes'): 80 | s.set('mimetypes', defaultMimeTypes) 81 | if not s.has('autorun'): 82 | s.set('autorun', defaultAutorun) 83 | if not s.has('defaultExtension'): 84 | s.set('defaultExtension', defaultExtension) 85 | 86 | # Normalize base path. 87 | if s.has('base'): 88 | base = s.get('base') 89 | base = base.replace('\\', '/') 90 | if not base.endswith('/'): 91 | base += '/' 92 | s.set('base', base) 93 | 94 | sublime.save_settings('SublimeServer.sublime-settings') 95 | 96 | # Merge project and user settings. 97 | window = sublime.active_window() 98 | if window: 99 | view = window.active_view() 100 | if view: 101 | settings = view.settings() 102 | if settings: 103 | serverSettings = settings.get('SublimeServer') 104 | if serverSettings: 105 | for setting in serverSettings: 106 | s.set(setting, serverSettings.get(setting)) 107 | 108 | return s 109 | 110 | 111 | def get_directories(): 112 | '''Get Open Directories in Sublime''' 113 | dic = {} 114 | # retrieve all Sublime windows 115 | windows = sublime.windows() 116 | for w in windows: 117 | # and retrieve all unique directory path 118 | fs = w.folders() 119 | for f in fs: 120 | key = f.split(os.path.sep)[-1] 121 | if key in dic.keys(): 122 | if dic[key] is f: 123 | continue 124 | else: 125 | loop = True 126 | num = 0 127 | while(loop): 128 | num += 1 129 | k = key + " " + str(num) 130 | if k in dic.keys(): 131 | if dic[k] is f: 132 | loop = False 133 | break 134 | else: 135 | dic[k] = f 136 | loop = False 137 | break 138 | else: 139 | dic[key] = f 140 | return dic 141 | 142 | 143 | class SublimeServerHandler(BaseHTTPRequestHandler): 144 | 145 | extensions_map = {} 146 | defaultExtension = None 147 | base_path = None 148 | 149 | def version_string(self): 150 | '''overwrite HTTP server's version string''' 151 | return 'SublimeServer/%s Sublime/%s' % (__VERSION__, sublime.version()) 152 | 153 | 154 | def do_GET(self): 155 | """Serve a GET request.""" 156 | 157 | # special case for .md files 158 | path = self.translate_path(self.path) 159 | if not os.path.isdir(path): 160 | ctype = self.guess_type(path) 161 | # ".md": "text/x-markdown; charset=UTF-8", 162 | if ctype and ctype.startswith("text/x-markdown"): 163 | self.send_md() 164 | return 165 | 166 | f = self.send_head() 167 | if f: 168 | try: 169 | self.copyfile(f, self.wfile) 170 | finally: 171 | f.close() 172 | 173 | def do_OPTIONS(self): 174 | """Serve a OPTIONS request.""" 175 | self.send_response(200, "ok") 176 | self.send_header('Access-Control-Allow-Origin', '*') 177 | self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS') 178 | self.send_header("Access-Control-Allow-Headers", "X-Requested-With") 179 | self.send_header("Access-Control-Allow-Headers", "Content-Type") 180 | self.end_headers() 181 | 182 | def do_POST(self): 183 | """Serve a POST request.""" 184 | self.do_GET() 185 | 186 | def do_PUT(self): 187 | """Serve a PUT request.""" 188 | self.do_GET() 189 | 190 | def do_HEAD(self): 191 | """Serve a HEAD request.""" 192 | f = self.send_head() 193 | if f: 194 | f.close() 195 | 196 | def send_head(self): 197 | path = self.translate_path(self.path) 198 | f = None 199 | if path is None: 200 | self.send_error(404, "File not found") 201 | return None 202 | if path is '/': 203 | return self.list_directory(path) 204 | if os.path.isdir(path): 205 | 206 | # not endswidth / 207 | if not self.path.endswith('/'): 208 | # redirect browser - doing basically what apache does 209 | self.send_response(301) 210 | self.send_header("Location", self.path + "/") 211 | self.end_headers() 212 | return None 213 | 214 | # looking for index.html or index.htm 215 | for index in "index.html", "index.htm": 216 | index = os.path.join(path, index) 217 | if os.path.exists(index): 218 | path = index 219 | break 220 | else: 221 | return self.list_directory(path) 222 | 223 | # If there's no extension and the file doesn't exist, 224 | # see if the file plus the default extension exists. 225 | if (SublimeServerHandler.defaultExtension and 226 | not posixpath.splitext(path)[1] and 227 | not posixpath.exists(path) and 228 | posixpath.exists(path + SublimeServerHandler.defaultExtension)): 229 | path += SublimeServerHandler.defaultExtension 230 | 231 | ctype = self.guess_type(path) 232 | try: 233 | # Always read in binary mode. Opening files in text mode may cause 234 | # newline translations, making the actual size of the content 235 | # transmitted *less* than the content-length! 236 | f = open(path, 'rb') 237 | except IOError: 238 | self.send_error(404, "File not found") 239 | return None 240 | try: 241 | self.send_response(200) 242 | self.send_header("Content-type", ctype) 243 | fs = os.fstat(f.fileno()) 244 | self.send_header("Content-Length", str(fs[6])) 245 | self.send_header("Access-Control-Allow-Origin", "*") 246 | self.send_header("Cache-Control", "no-cache, no-store, must-revalidate") 247 | self.send_header("Pragma", "no-cache") 248 | self.send_header("Expires", "0") 249 | self.send_header( 250 | "Last-Modified", self.date_time_string(fs.st_mtime)) 251 | self.end_headers() 252 | return f 253 | except: 254 | f.close() 255 | raise 256 | 257 | def send_md(self): 258 | path = self.translate_path(self.path) 259 | try: 260 | TEMPLATE = """ 261 | 262 | 263 | 264 |
265 | 266 | 267 | 274 | 275 | 276 | """ 277 | html = TEMPLATE % open(path,"r",encoding='utf8').read() 278 | except IOError: 279 | self.send_error(404, "File not found") 280 | return None 281 | encoded = html.encode(sys.getfilesystemencoding()) 282 | f = io.BytesIO() 283 | f.write(encoded) 284 | f.seek(0) 285 | self.send_response(200) 286 | self.send_header("Content-type", "text/html") 287 | self.send_header("Content-Length", len(encoded)) 288 | self.send_header("Last-Modified", time.time()) 289 | self.end_headers() 290 | 291 | self.copyfile(f, self.wfile) 292 | f.close() 293 | # self.wfile.write(html) 294 | 295 | # Maybe we can using template 296 | def list_directory(self, path): 297 | global dic 298 | # a flag to mark if current directory is root 299 | root = False 300 | # request the root directory 301 | # and show the open directories in sublime 302 | if path is '/': 303 | root = True 304 | else: 305 | try: 306 | list = os.listdir(path) 307 | except os.error: 308 | self.send_error(403, "Access Denied") 309 | return None 310 | list.sort(key=lambda a: a.lower()) 311 | # f = StringIO() 312 | r = [] 313 | displaypath = cgi.escape(urllib.unquote(self.path)) 314 | enc = sys.getfilesystemencoding() 315 | r.append('') 316 | r.append('\n\n\n' % enc) 317 | r.append('SublimeServer %s\n\n' % displaypath) 318 | r.append('') 319 | r.append('\n

SublimeServer %s

\n' % displaypath) 320 | r.append('
\n\n
\n\n\n") 339 | encoded = ''.join(r).encode(enc) 340 | f = io.BytesIO() 341 | f.write(encoded) 342 | # length = f.tell() 343 | f.seek(0) 344 | self.send_response(200) 345 | # encoding = sys.getfilesystemencoding() 346 | self.send_header("Content-type", "text/html; charset=%s" % enc) 347 | self.send_header("Content-Length", str(encoded)) 348 | self.end_headers() 349 | return f 350 | 351 | def translate_path(self, path): 352 | global dic 353 | # abandon query parameters 354 | path = path.split('?', 1)[0] 355 | path = path.split('#', 1)[0] 356 | # Don't forget explicit trailing slash when normalizing. Issue17324 357 | trailing_slash = path.rstrip().endswith('/') 358 | path = posixpath.normpath(urllib.unquote(path)) 359 | if path == '/': 360 | sublime.set_timeout( 361 | lambda: sublime.run_command('sublimeserver_reload'), 0) 362 | # sleep 0.001 second to wait SublimeserverReloadCommand done 363 | # any else sulotion? 364 | time.sleep(0.001) 365 | return path 366 | # the browser try to get favourite icon 367 | if path == '/favicon.ico': 368 | return sublime.packages_path() + "/SublimeServer/favicon.ico" 369 | 370 | elif path == '/SublimeServer.css': 371 | return sublime.packages_path() + "/SublimeServer/style.example.css" 372 | 373 | # markdown java script from https://github.com/evilstreak/markdown-js 374 | elif path == '/markdown.js': 375 | return sublime.packages_path() + "/SublimeServer/markdown.js" 376 | 377 | if SublimeServerHandler.base_path: 378 | path = SublimeServerHandler.base_path + path 379 | 380 | # else, deal with path... 381 | words = path.split('/') 382 | words = filter(None, words) 383 | if python_version == 3: 384 | tmp = [] 385 | try: 386 | while True: 387 | tmp.append(next(words)) 388 | except StopIteration: 389 | words = tmp 390 | 391 | if words[0] in dic: 392 | path = dic[words[0]] 393 | for word in words[1:]: 394 | path = os.path.join(path, word) 395 | return path 396 | else: 397 | return None 398 | 399 | def copyfile(self, source, outputfile): 400 | shutil.copyfileobj(source, outputfile) 401 | 402 | def guess_type(self, path): 403 | base, ext = posixpath.splitext(path) 404 | if ext in SublimeServerHandler.extensions_map: 405 | return SublimeServerHandler.extensions_map[ext] 406 | ext = ext.lower() 407 | if ext in SublimeServerHandler.extensions_map: 408 | return SublimeServerHandler.extensions_map[ext] 409 | else: 410 | return SublimeServerHandler.extensions_map[''] 411 | 412 | 413 | class SublimeServerThreadMixIn(ThreadingMixIn, TCPServer): 414 | pass 415 | 416 | 417 | class SublimeServerThread(threading.Thread): 418 | httpd = None 419 | 420 | def __init__(self): 421 | settings = load_settings() 422 | super(SublimeServerThread, self).__init__() 423 | if not mimetypes.inited: 424 | mimetypes.init() # try to read system mime.types 425 | SublimeServerHandler.extensions_map = mimetypes.types_map.copy() 426 | SublimeServerHandler.extensions_map.update(settings.get('mimetypes')) 427 | SublimeServerHandler.base_path = settings.get('base') 428 | SublimeServerHandler.defaultExtension = settings.get('defaultExtension') 429 | self.httpd = SublimeServerThreadMixIn(('', settings.get('port')), SublimeServerHandler) 430 | 431 | self.setName(self.__class__.__name__) 432 | 433 | def run(self): 434 | self.httpd.serve_forever() 435 | 436 | def stop(self): 437 | self.httpd.shutdown() 438 | self.httpd.server_close() 439 | 440 | 441 | class SublimeserverStartCommand(sublime_plugin.ApplicationCommand): 442 | def run(self): 443 | global settings, thread, dic, attempts 444 | settings = load_settings() 445 | if thread is not None and thread.is_alive(): 446 | return sublime.message_dialog('SublimeServer Alread Started!') 447 | try: 448 | dic = get_directories() 449 | thread = SublimeServerThread() 450 | thread.start() 451 | sublime.status_message('SublimeServer Started!') 452 | attempts = 0 453 | except socket.error as error: 454 | attempts += 1 455 | if attempts > settings.get('attempts'): 456 | # max attempts reached 457 | # reset attempts to 0 458 | attempts = 0 459 | sublime.message_dialog('Unknow Error') 460 | # try: 461 | # sublime.message_dialog(message) 462 | # except UnicodeDecodeError: 463 | # sublime.message_dialog(message.decode(sys.getfilesystemencoding())) 464 | else: 465 | # try another attempt 466 | sublime.set_timeout( 467 | lambda: sublime.run_command('sublimeserver_start'), 468 | settings.get('interval')) 469 | 470 | def is_enabled(self): 471 | global thread 472 | return not (thread is not None and thread.is_alive()) 473 | 474 | 475 | class SublimeserverStopCommand(sublime_plugin.ApplicationCommand): 476 | def run(self): 477 | global thread 478 | if thread is not None and thread.is_alive(): 479 | thread.stop() 480 | thread.join() 481 | thread = None 482 | sublime.status_message('SublimeServer Stopped!') 483 | 484 | def is_enabled(self): 485 | global thread 486 | return thread is not None and thread.is_alive() 487 | 488 | 489 | class SublimeserverRestartCommand(sublime_plugin.ApplicationCommand): 490 | def run(self): 491 | global settings 492 | sublime.run_command('sublimeserver_stop') 493 | sublime.set_timeout( 494 | lambda: sublime.run_command('sublimeserver_reload'), 0) 495 | sublime.set_timeout( 496 | lambda: sublime.run_command('sublimeserver_start'), 497 | settings.get('interval')) 498 | 499 | def is_enabled(self): 500 | global thread 501 | return thread is not None and thread.is_alive() 502 | 503 | 504 | class SublimeserverReloadCommand(sublime_plugin.ApplicationCommand): 505 | def run(self): 506 | global dic, get_directories, settings, load_settings 507 | dic = get_directories() 508 | settings = load_settings() 509 | 510 | 511 | class SublimeserverBrowserCommand(sublime_plugin.TextCommand): 512 | def run(self, edit): 513 | global dic, settings, thread, get_directories 514 | settings = load_settings() 515 | if thread is None or not thread.is_alive(): 516 | return sublime.message_dialog('SublimeServer isn\'t Started yet!') 517 | # if dic is None: 518 | dic = get_directories() 519 | url = "http://localhost:{0}/{1}" 520 | filename = self.view.file_name() 521 | base = settings.get('base') 522 | # Find the file. 523 | for k in dic: 524 | if filename.startswith(dic[k]): 525 | path = k + filename[len(dic[k]):] 526 | # Normalize path for Windows users. 527 | path = path.replace('\\', '/') 528 | # Remove base path from URL. It's assumed by server. 529 | if base and path.startswith(base): 530 | path = path[len(base):] 531 | url = url.format(settings.get('port'), path) 532 | return webbrowser.open(url) 533 | rawname = filename.split(os.path.sep)[-1] 534 | sublime.message_dialog( 535 | 'File %s not in Sublime Project Folder!' % rawname) 536 | 537 | def is_enabled(self): 538 | global thread 539 | return thread is not None and thread.is_alive() 540 | 541 | 542 | class SublimeserverAutorun(sublime_plugin.EventListener): 543 | def on_activated(self, view): 544 | global loaded, settings 545 | if loaded: 546 | return 547 | loaded = True 548 | # if autorun set to True 549 | if settings.get('autorun') and thread is None: 550 | sublime.run_command('sublimeserver_start') 551 | 552 | # load settings now 553 | settings = load_settings() 554 | # For ST3 555 | def plugin_loaded(): 556 | global settings 557 | settings = load_settings() 558 | 559 | # check if last SublimeServerThread exists 560 | threads = threading.enumerate() 561 | for t in threads: 562 | if t.__class__.__name__ is SublimeServerThread.__name__: 563 | thread = t 564 | break 565 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learning/SublimeServer/466510ba53ddb569ae6b7cd90cfaf6616c4daca5/favicon.ico -------------------------------------------------------------------------------- /markdown.js: -------------------------------------------------------------------------------- 1 | // Released under MIT license 2 | // Copyright (c) 2009-2010 Dominic Baggott 3 | // Copyright (c) 2009-2010 Ash Berlin 4 | // Copyright (c) 2011 Christoph Dorn (http://www.christophdorn.com) 5 | 6 | /*jshint browser:true, devel:true */ 7 | 8 | (function( expose ) { 9 | 10 | /** 11 | * class Markdown 12 | * 13 | * Markdown processing in Javascript done right. We have very particular views 14 | * on what constitutes 'right' which include: 15 | * 16 | * - produces well-formed HTML (this means that em and strong nesting is 17 | * important) 18 | * 19 | * - has an intermediate representation to allow processing of parsed data (We 20 | * in fact have two, both as [JsonML]: a markdown tree and an HTML tree). 21 | * 22 | * - is easily extensible to add new dialects without having to rewrite the 23 | * entire parsing mechanics 24 | * 25 | * - has a good test suite 26 | * 27 | * This implementation fulfills all of these (except that the test suite could 28 | * do with expanding to automatically run all the fixtures from other Markdown 29 | * implementations.) 30 | * 31 | * ##### Intermediate Representation 32 | * 33 | * *TODO* Talk about this :) Its JsonML, but document the node names we use. 34 | * 35 | * [JsonML]: http://jsonml.org/ "JSON Markup Language" 36 | **/ 37 | var Markdown = expose.Markdown = function(dialect) { 38 | switch (typeof dialect) { 39 | case "undefined": 40 | this.dialect = Markdown.dialects.Gruber; 41 | break; 42 | case "object": 43 | this.dialect = dialect; 44 | break; 45 | default: 46 | if ( dialect in Markdown.dialects ) { 47 | this.dialect = Markdown.dialects[dialect]; 48 | } 49 | else { 50 | throw new Error("Unknown Markdown dialect '" + String(dialect) + "'"); 51 | } 52 | break; 53 | } 54 | this.em_state = []; 55 | this.strong_state = []; 56 | this.debug_indent = ""; 57 | }; 58 | 59 | /** 60 | * parse( markdown, [dialect] ) -> JsonML 61 | * - markdown (String): markdown string to parse 62 | * - dialect (String | Dialect): the dialect to use, defaults to gruber 63 | * 64 | * Parse `markdown` and return a markdown document as a Markdown.JsonML tree. 65 | **/ 66 | expose.parse = function( source, dialect ) { 67 | // dialect will default if undefined 68 | var md = new Markdown( dialect ); 69 | return md.toTree( source ); 70 | }; 71 | 72 | /** 73 | * toHTML( markdown, [dialect] ) -> String 74 | * toHTML( md_tree ) -> String 75 | * - markdown (String): markdown string to parse 76 | * - md_tree (Markdown.JsonML): parsed markdown tree 77 | * 78 | * Take markdown (either as a string or as a JsonML tree) and run it through 79 | * [[toHTMLTree]] then turn it into a well-formated HTML fragment. 80 | **/ 81 | expose.toHTML = function toHTML( source , dialect , options ) { 82 | var input = expose.toHTMLTree( source , dialect , options ); 83 | 84 | return expose.renderJsonML( input ); 85 | }; 86 | 87 | /** 88 | * toHTMLTree( markdown, [dialect] ) -> JsonML 89 | * toHTMLTree( md_tree ) -> JsonML 90 | * - markdown (String): markdown string to parse 91 | * - dialect (String | Dialect): the dialect to use, defaults to gruber 92 | * - md_tree (Markdown.JsonML): parsed markdown tree 93 | * 94 | * Turn markdown into HTML, represented as a JsonML tree. If a string is given 95 | * to this function, it is first parsed into a markdown tree by calling 96 | * [[parse]]. 97 | **/ 98 | expose.toHTMLTree = function toHTMLTree( input, dialect , options ) { 99 | // convert string input to an MD tree 100 | if ( typeof input ==="string" ) input = this.parse( input, dialect ); 101 | 102 | // Now convert the MD tree to an HTML tree 103 | 104 | // remove references from the tree 105 | var attrs = extract_attr( input ), 106 | refs = {}; 107 | 108 | if ( attrs && attrs.references ) { 109 | refs = attrs.references; 110 | } 111 | 112 | var html = convert_tree_to_html( input, refs , options ); 113 | merge_text_nodes( html ); 114 | return html; 115 | }; 116 | 117 | // For Spidermonkey based engines 118 | function mk_block_toSource() { 119 | return "Markdown.mk_block( " + 120 | uneval(this.toString()) + 121 | ", " + 122 | uneval(this.trailing) + 123 | ", " + 124 | uneval(this.lineNumber) + 125 | " )"; 126 | } 127 | 128 | // node 129 | function mk_block_inspect() { 130 | var util = require("util"); 131 | return "Markdown.mk_block( " + 132 | util.inspect(this.toString()) + 133 | ", " + 134 | util.inspect(this.trailing) + 135 | ", " + 136 | util.inspect(this.lineNumber) + 137 | " )"; 138 | 139 | } 140 | 141 | var mk_block = Markdown.mk_block = function(block, trail, line) { 142 | // Be helpful for default case in tests. 143 | if ( arguments.length == 1 ) trail = "\n\n"; 144 | 145 | var s = new String(block); 146 | s.trailing = trail; 147 | // To make it clear its not just a string 148 | s.inspect = mk_block_inspect; 149 | s.toSource = mk_block_toSource; 150 | 151 | if ( line != undefined ) 152 | s.lineNumber = line; 153 | 154 | return s; 155 | }; 156 | 157 | function count_lines( str ) { 158 | var n = 0, i = -1; 159 | while ( ( i = str.indexOf("\n", i + 1) ) !== -1 ) n++; 160 | return n; 161 | } 162 | 163 | // Internal - split source into rough blocks 164 | Markdown.prototype.split_blocks = function splitBlocks( input, startLine ) { 165 | input = input.replace(/(\r\n|\n|\r)/g, "\n"); 166 | // [\s\S] matches _anything_ (newline or space) 167 | var re = /([\s\S]+?)($|\n(?:\s*\n|$)+)/g, 168 | blocks = [], 169 | m; 170 | 171 | var line_no = 1; 172 | 173 | if ( ( m = /^(\s*\n)/.exec(input) ) != null ) { 174 | // skip (but count) leading blank lines 175 | line_no += count_lines( m[0] ); 176 | re.lastIndex = m[0].length; 177 | } 178 | 179 | while ( ( m = re.exec(input) ) !== null ) { 180 | blocks.push( mk_block( m[1], m[2], line_no ) ); 181 | line_no += count_lines( m[0] ); 182 | } 183 | 184 | return blocks; 185 | }; 186 | 187 | /** 188 | * Markdown#processBlock( block, next ) -> undefined | [ JsonML, ... ] 189 | * - block (String): the block to process 190 | * - next (Array): the following blocks 191 | * 192 | * Process `block` and return an array of JsonML nodes representing `block`. 193 | * 194 | * It does this by asking each block level function in the dialect to process 195 | * the block until one can. Succesful handling is indicated by returning an 196 | * array (with zero or more JsonML nodes), failure by a false value. 197 | * 198 | * Blocks handlers are responsible for calling [[Markdown#processInline]] 199 | * themselves as appropriate. 200 | * 201 | * If the blocks were split incorrectly or adjacent blocks need collapsing you 202 | * can adjust `next` in place using shift/splice etc. 203 | * 204 | * If any of this default behaviour is not right for the dialect, you can 205 | * define a `__call__` method on the dialect that will get invoked to handle 206 | * the block processing. 207 | */ 208 | Markdown.prototype.processBlock = function processBlock( block, next ) { 209 | var cbs = this.dialect.block, 210 | ord = cbs.__order__; 211 | 212 | if ( "__call__" in cbs ) { 213 | return cbs.__call__.call(this, block, next); 214 | } 215 | 216 | for ( var i = 0; i < ord.length; i++ ) { 217 | //D:this.debug( "Testing", ord[i] ); 218 | var res = cbs[ ord[i] ].call( this, block, next ); 219 | if ( res ) { 220 | //D:this.debug(" matched"); 221 | if ( !isArray(res) || ( res.length > 0 && !( isArray(res[0]) ) ) ) 222 | this.debug(ord[i], "didn't return a proper array"); 223 | //D:this.debug( "" ); 224 | return res; 225 | } 226 | } 227 | 228 | // Uhoh! no match! Should we throw an error? 229 | return []; 230 | }; 231 | 232 | Markdown.prototype.processInline = function processInline( block ) { 233 | return this.dialect.inline.__call__.call( this, String( block ) ); 234 | }; 235 | 236 | /** 237 | * Markdown#toTree( source ) -> JsonML 238 | * - source (String): markdown source to parse 239 | * 240 | * Parse `source` into a JsonML tree representing the markdown document. 241 | **/ 242 | // custom_tree means set this.tree to `custom_tree` and restore old value on return 243 | Markdown.prototype.toTree = function toTree( source, custom_root ) { 244 | var blocks = source instanceof Array ? source : this.split_blocks( source ); 245 | 246 | // Make tree a member variable so its easier to mess with in extensions 247 | var old_tree = this.tree; 248 | try { 249 | this.tree = custom_root || this.tree || [ "markdown" ]; 250 | 251 | blocks: 252 | while ( blocks.length ) { 253 | var b = this.processBlock( blocks.shift(), blocks ); 254 | 255 | // Reference blocks and the like won't return any content 256 | if ( !b.length ) continue blocks; 257 | 258 | this.tree.push.apply( this.tree, b ); 259 | } 260 | return this.tree; 261 | } 262 | finally { 263 | if ( custom_root ) { 264 | this.tree = old_tree; 265 | } 266 | } 267 | }; 268 | 269 | // Noop by default 270 | Markdown.prototype.debug = function () { 271 | var args = Array.prototype.slice.call( arguments); 272 | args.unshift(this.debug_indent); 273 | if ( typeof print !== "undefined" ) 274 | print.apply( print, args ); 275 | if ( typeof console !== "undefined" && typeof console.log !== "undefined" ) 276 | console.log.apply( null, args ); 277 | } 278 | 279 | Markdown.prototype.loop_re_over_block = function( re, block, cb ) { 280 | // Dont use /g regexps with this 281 | var m, 282 | b = block.valueOf(); 283 | 284 | while ( b.length && (m = re.exec(b) ) != null ) { 285 | b = b.substr( m[0].length ); 286 | cb.call(this, m); 287 | } 288 | return b; 289 | }; 290 | 291 | /** 292 | * Markdown.dialects 293 | * 294 | * Namespace of built-in dialects. 295 | **/ 296 | Markdown.dialects = {}; 297 | 298 | /** 299 | * Markdown.dialects.Gruber 300 | * 301 | * The default dialect that follows the rules set out by John Gruber's 302 | * markdown.pl as closely as possible. Well actually we follow the behaviour of 303 | * that script which in some places is not exactly what the syntax web page 304 | * says. 305 | **/ 306 | Markdown.dialects.Gruber = { 307 | block: { 308 | atxHeader: function atxHeader( block, next ) { 309 | var m = block.match( /^(#{1,6})\s*(.*?)\s*#*\s*(?:\n|$)/ ); 310 | 311 | if ( !m ) return undefined; 312 | 313 | var header = [ "header", { level: m[ 1 ].length } ]; 314 | Array.prototype.push.apply(header, this.processInline(m[ 2 ])); 315 | 316 | if ( m[0].length < block.length ) 317 | next.unshift( mk_block( block.substr( m[0].length ), block.trailing, block.lineNumber + 2 ) ); 318 | 319 | return [ header ]; 320 | }, 321 | 322 | setextHeader: function setextHeader( block, next ) { 323 | var m = block.match( /^(.*)\n([-=])\2\2+(?:\n|$)/ ); 324 | 325 | if ( !m ) return undefined; 326 | 327 | var level = ( m[ 2 ] === "=" ) ? 1 : 2; 328 | var header = [ "header", { level : level }, m[ 1 ] ]; 329 | 330 | if ( m[0].length < block.length ) 331 | next.unshift( mk_block( block.substr( m[0].length ), block.trailing, block.lineNumber + 2 ) ); 332 | 333 | return [ header ]; 334 | }, 335 | 336 | code: function code( block, next ) { 337 | // | Foo 338 | // |bar 339 | // should be a code block followed by a paragraph. Fun 340 | // 341 | // There might also be adjacent code block to merge. 342 | 343 | var ret = [], 344 | re = /^(?: {0,3}\t| {4})(.*)\n?/, 345 | lines; 346 | 347 | // 4 spaces + content 348 | if ( !block.match( re ) ) return undefined; 349 | 350 | block_search: 351 | do { 352 | // Now pull out the rest of the lines 353 | var b = this.loop_re_over_block( 354 | re, block.valueOf(), function( m ) { ret.push( m[1] ); } ); 355 | 356 | if ( b.length ) { 357 | // Case alluded to in first comment. push it back on as a new block 358 | next.unshift( mk_block(b, block.trailing) ); 359 | break block_search; 360 | } 361 | else if ( next.length ) { 362 | // Check the next block - it might be code too 363 | if ( !next[0].match( re ) ) break block_search; 364 | 365 | // Pull how how many blanks lines follow - minus two to account for .join 366 | ret.push ( block.trailing.replace(/[^\n]/g, "").substring(2) ); 367 | 368 | block = next.shift(); 369 | } 370 | else { 371 | break block_search; 372 | } 373 | } while ( true ); 374 | 375 | return [ [ "code_block", ret.join("\n") ] ]; 376 | }, 377 | 378 | horizRule: function horizRule( block, next ) { 379 | // this needs to find any hr in the block to handle abutting blocks 380 | var m = block.match( /^(?:([\s\S]*?)\n)?[ \t]*([-_*])(?:[ \t]*\2){2,}[ \t]*(?:\n([\s\S]*))?$/ ); 381 | 382 | if ( !m ) { 383 | return undefined; 384 | } 385 | 386 | var jsonml = [ [ "hr" ] ]; 387 | 388 | // if there's a leading abutting block, process it 389 | if ( m[ 1 ] ) { 390 | jsonml.unshift.apply( jsonml, this.processBlock( m[ 1 ], [] ) ); 391 | } 392 | 393 | // if there's a trailing abutting block, stick it into next 394 | if ( m[ 3 ] ) { 395 | next.unshift( mk_block( m[ 3 ] ) ); 396 | } 397 | 398 | return jsonml; 399 | }, 400 | 401 | // There are two types of lists. Tight and loose. Tight lists have no whitespace 402 | // between the items (and result in text just in the
  • ) and loose lists, 403 | // which have an empty line between list items, resulting in (one or more) 404 | // paragraphs inside the
  • . 405 | // 406 | // There are all sorts weird edge cases about the original markdown.pl's 407 | // handling of lists: 408 | // 409 | // * Nested lists are supposed to be indented by four chars per level. But 410 | // if they aren't, you can get a nested list by indenting by less than 411 | // four so long as the indent doesn't match an indent of an existing list 412 | // item in the 'nest stack'. 413 | // 414 | // * The type of the list (bullet or number) is controlled just by the 415 | // first item at the indent. Subsequent changes are ignored unless they 416 | // are for nested lists 417 | // 418 | lists: (function( ) { 419 | // Use a closure to hide a few variables. 420 | var any_list = "[*+-]|\\d+\\.", 421 | bullet_list = /[*+-]/, 422 | number_list = /\d+\./, 423 | // Capture leading indent as it matters for determining nested lists. 424 | is_list_re = new RegExp( "^( {0,3})(" + any_list + ")[ \t]+" ), 425 | indent_re = "(?: {0,3}\\t| {4})"; 426 | 427 | // TODO: Cache this regexp for certain depths. 428 | // Create a regexp suitable for matching an li for a given stack depth 429 | function regex_for_depth( depth ) { 430 | 431 | return new RegExp( 432 | // m[1] = indent, m[2] = list_type 433 | "(?:^(" + indent_re + "{0," + depth + "} {0,3})(" + any_list + ")\\s+)|" + 434 | // m[3] = cont 435 | "(^" + indent_re + "{0," + (depth-1) + "}[ ]{0,4})" 436 | ); 437 | } 438 | function expand_tab( input ) { 439 | return input.replace( / {0,3}\t/g, " " ); 440 | } 441 | 442 | // Add inline content `inline` to `li`. inline comes from processInline 443 | // so is an array of content 444 | function add(li, loose, inline, nl) { 445 | if ( loose ) { 446 | li.push( [ "para" ].concat(inline) ); 447 | return; 448 | } 449 | // Hmmm, should this be any block level element or just paras? 450 | var add_to = li[li.length -1] instanceof Array && li[li.length - 1][0] == "para" 451 | ? li[li.length -1] 452 | : li; 453 | 454 | // If there is already some content in this list, add the new line in 455 | if ( nl && li.length > 1 ) inline.unshift(nl); 456 | 457 | for ( var i = 0; i < inline.length; i++ ) { 458 | var what = inline[i], 459 | is_str = typeof what == "string"; 460 | if ( is_str && add_to.length > 1 && typeof add_to[add_to.length-1] == "string" ) { 461 | add_to[ add_to.length-1 ] += what; 462 | } 463 | else { 464 | add_to.push( what ); 465 | } 466 | } 467 | } 468 | 469 | // contained means have an indent greater than the current one. On 470 | // *every* line in the block 471 | function get_contained_blocks( depth, blocks ) { 472 | 473 | var re = new RegExp( "^(" + indent_re + "{" + depth + "}.*?\\n?)*$" ), 474 | replace = new RegExp("^" + indent_re + "{" + depth + "}", "gm"), 475 | ret = []; 476 | 477 | while ( blocks.length > 0 ) { 478 | if ( re.exec( blocks[0] ) ) { 479 | var b = blocks.shift(), 480 | // Now remove that indent 481 | x = b.replace( replace, ""); 482 | 483 | ret.push( mk_block( x, b.trailing, b.lineNumber ) ); 484 | } 485 | else { 486 | break; 487 | } 488 | } 489 | return ret; 490 | } 491 | 492 | // passed to stack.forEach to turn list items up the stack into paras 493 | function paragraphify(s, i, stack) { 494 | var list = s.list; 495 | var last_li = list[list.length-1]; 496 | 497 | if ( last_li[1] instanceof Array && last_li[1][0] == "para" ) { 498 | return; 499 | } 500 | if ( i + 1 == stack.length ) { 501 | // Last stack frame 502 | // Keep the same array, but replace the contents 503 | last_li.push( ["para"].concat( last_li.splice(1, last_li.length - 1) ) ); 504 | } 505 | else { 506 | var sublist = last_li.pop(); 507 | last_li.push( ["para"].concat( last_li.splice(1, last_li.length - 1) ), sublist ); 508 | } 509 | } 510 | 511 | // The matcher function 512 | return function( block, next ) { 513 | var m = block.match( is_list_re ); 514 | if ( !m ) return undefined; 515 | 516 | function make_list( m ) { 517 | var list = bullet_list.exec( m[2] ) 518 | ? ["bulletlist"] 519 | : ["numberlist"]; 520 | 521 | stack.push( { list: list, indent: m[1] } ); 522 | return list; 523 | } 524 | 525 | 526 | var stack = [], // Stack of lists for nesting. 527 | list = make_list( m ), 528 | last_li, 529 | loose = false, 530 | ret = [ stack[0].list ], 531 | i; 532 | 533 | // Loop to search over block looking for inner block elements and loose lists 534 | loose_search: 535 | while ( true ) { 536 | // Split into lines preserving new lines at end of line 537 | var lines = block.split( /(?=\n)/ ); 538 | 539 | // We have to grab all lines for a li and call processInline on them 540 | // once as there are some inline things that can span lines. 541 | var li_accumulate = ""; 542 | 543 | // Loop over the lines in this block looking for tight lists. 544 | tight_search: 545 | for ( var line_no = 0; line_no < lines.length; line_no++ ) { 546 | var nl = "", 547 | l = lines[line_no].replace(/^\n/, function(n) { nl = n; return ""; }); 548 | 549 | // TODO: really should cache this 550 | var line_re = regex_for_depth( stack.length ); 551 | 552 | m = l.match( line_re ); 553 | //print( "line:", uneval(l), "\nline match:", uneval(m) ); 554 | 555 | // We have a list item 556 | if ( m[1] !== undefined ) { 557 | // Process the previous list item, if any 558 | if ( li_accumulate.length ) { 559 | add( last_li, loose, this.processInline( li_accumulate ), nl ); 560 | // Loose mode will have been dealt with. Reset it 561 | loose = false; 562 | li_accumulate = ""; 563 | } 564 | 565 | m[1] = expand_tab( m[1] ); 566 | var wanted_depth = Math.floor(m[1].length/4)+1; 567 | //print( "want:", wanted_depth, "stack:", stack.length); 568 | if ( wanted_depth > stack.length ) { 569 | // Deep enough for a nested list outright 570 | //print ( "new nested list" ); 571 | list = make_list( m ); 572 | last_li.push( list ); 573 | last_li = list[1] = [ "listitem" ]; 574 | } 575 | else { 576 | // We aren't deep enough to be strictly a new level. This is 577 | // where Md.pl goes nuts. If the indent matches a level in the 578 | // stack, put it there, else put it one deeper then the 579 | // wanted_depth deserves. 580 | var found = false; 581 | for ( i = 0; i < stack.length; i++ ) { 582 | if ( stack[ i ].indent != m[1] ) continue; 583 | list = stack[ i ].list; 584 | stack.splice( i+1, stack.length - (i+1) ); 585 | found = true; 586 | break; 587 | } 588 | 589 | if (!found) { 590 | //print("not found. l:", uneval(l)); 591 | wanted_depth++; 592 | if ( wanted_depth <= stack.length ) { 593 | stack.splice(wanted_depth, stack.length - wanted_depth); 594 | //print("Desired depth now", wanted_depth, "stack:", stack.length); 595 | list = stack[wanted_depth-1].list; 596 | //print("list:", uneval(list) ); 597 | } 598 | else { 599 | //print ("made new stack for messy indent"); 600 | list = make_list(m); 601 | last_li.push(list); 602 | } 603 | } 604 | 605 | //print( uneval(list), "last", list === stack[stack.length-1].list ); 606 | last_li = [ "listitem" ]; 607 | list.push(last_li); 608 | } // end depth of shenegains 609 | nl = ""; 610 | } 611 | 612 | // Add content 613 | if ( l.length > m[0].length ) { 614 | li_accumulate += nl + l.substr( m[0].length ); 615 | } 616 | } // tight_search 617 | 618 | if ( li_accumulate.length ) { 619 | add( last_li, loose, this.processInline( li_accumulate ), nl ); 620 | // Loose mode will have been dealt with. Reset it 621 | loose = false; 622 | li_accumulate = ""; 623 | } 624 | 625 | // Look at the next block - we might have a loose list. Or an extra 626 | // paragraph for the current li 627 | var contained = get_contained_blocks( stack.length, next ); 628 | 629 | // Deal with code blocks or properly nested lists 630 | if ( contained.length > 0 ) { 631 | // Make sure all listitems up the stack are paragraphs 632 | forEach( stack, paragraphify, this); 633 | 634 | last_li.push.apply( last_li, this.toTree( contained, [] ) ); 635 | } 636 | 637 | var next_block = next[0] && next[0].valueOf() || ""; 638 | 639 | if ( next_block.match(is_list_re) || next_block.match( /^ / ) ) { 640 | block = next.shift(); 641 | 642 | // Check for an HR following a list: features/lists/hr_abutting 643 | var hr = this.dialect.block.horizRule( block, next ); 644 | 645 | if ( hr ) { 646 | ret.push.apply(ret, hr); 647 | break; 648 | } 649 | 650 | // Make sure all listitems up the stack are paragraphs 651 | forEach( stack, paragraphify, this); 652 | 653 | loose = true; 654 | continue loose_search; 655 | } 656 | break; 657 | } // loose_search 658 | 659 | return ret; 660 | }; 661 | })(), 662 | 663 | blockquote: function blockquote( block, next ) { 664 | if ( !block.match( /^>/m ) ) 665 | return undefined; 666 | 667 | var jsonml = []; 668 | 669 | // separate out the leading abutting block, if any 670 | if ( block[ 0 ] != ">" ) { 671 | var lines = block.split( /\n/ ), 672 | prev = []; 673 | 674 | // keep shifting lines until you find a crotchet 675 | while ( lines.length && lines[ 0 ][ 0 ] != ">" ) { 676 | prev.push( lines.shift() ); 677 | } 678 | 679 | // reassemble! 680 | block = lines.join( "\n" ); 681 | jsonml.push.apply( jsonml, this.processBlock( prev.join( "\n" ), [] ) ); 682 | } 683 | 684 | // if the next block is also a blockquote merge it in 685 | while ( next.length && next[ 0 ][ 0 ] == ">" ) { 686 | var b = next.shift(); 687 | block = new String(block + block.trailing + b); 688 | block.trailing = b.trailing; 689 | } 690 | 691 | // Strip off the leading "> " and re-process as a block. 692 | var input = block.replace( /^> ?/gm, "" ), 693 | old_tree = this.tree, 694 | processedBlock = this.toTree( input, [ "blockquote" ] ), 695 | attr = extract_attr( processedBlock ); 696 | 697 | // If any link references were found get rid of them 698 | if ( attr && attr.references ) { 699 | delete attr.references; 700 | // And then remove the attribute object if it's empty 701 | if ( isEmpty( attr ) ) { 702 | processedBlock.splice( 1, 1 ); 703 | } 704 | } 705 | 706 | jsonml.push( processedBlock ); 707 | return jsonml; 708 | }, 709 | 710 | referenceDefn: function referenceDefn( block, next) { 711 | var re = /^\s*\[(.*?)\]:\s*(\S+)(?:\s+(?:(['"])(.*?)\3|\((.*?)\)))?\n?/; 712 | // interesting matches are [ , ref_id, url, , title, title ] 713 | 714 | if ( !block.match(re) ) 715 | return undefined; 716 | 717 | // make an attribute node if it doesn't exist 718 | if ( !extract_attr( this.tree ) ) { 719 | this.tree.splice( 1, 0, {} ); 720 | } 721 | 722 | var attrs = extract_attr( this.tree ); 723 | 724 | // make a references hash if it doesn't exist 725 | if ( attrs.references === undefined ) { 726 | attrs.references = {}; 727 | } 728 | 729 | var b = this.loop_re_over_block(re, block, function( m ) { 730 | 731 | if ( m[2] && m[2][0] == "<" && m[2][m[2].length-1] == ">" ) 732 | m[2] = m[2].substring( 1, m[2].length - 1 ); 733 | 734 | var ref = attrs.references[ m[1].toLowerCase() ] = { 735 | href: m[2] 736 | }; 737 | 738 | if ( m[4] !== undefined ) 739 | ref.title = m[4]; 740 | else if ( m[5] !== undefined ) 741 | ref.title = m[5]; 742 | 743 | } ); 744 | 745 | if ( b.length ) 746 | next.unshift( mk_block( b, block.trailing ) ); 747 | 748 | return []; 749 | }, 750 | 751 | para: function para( block, next ) { 752 | // everything's a para! 753 | return [ ["para"].concat( this.processInline( block ) ) ]; 754 | } 755 | } 756 | }; 757 | 758 | Markdown.dialects.Gruber.inline = { 759 | 760 | __oneElement__: function oneElement( text, patterns_or_re, previous_nodes ) { 761 | var m, 762 | res, 763 | lastIndex = 0; 764 | 765 | patterns_or_re = patterns_or_re || this.dialect.inline.__patterns__; 766 | var re = new RegExp( "([\\s\\S]*?)(" + (patterns_or_re.source || patterns_or_re) + ")" ); 767 | 768 | m = re.exec( text ); 769 | if (!m) { 770 | // Just boring text 771 | return [ text.length, text ]; 772 | } 773 | else if ( m[1] ) { 774 | // Some un-interesting text matched. Return that first 775 | return [ m[1].length, m[1] ]; 776 | } 777 | 778 | var res; 779 | if ( m[2] in this.dialect.inline ) { 780 | res = this.dialect.inline[ m[2] ].call( 781 | this, 782 | text.substr( m.index ), m, previous_nodes || [] ); 783 | } 784 | // Default for now to make dev easier. just slurp special and output it. 785 | res = res || [ m[2].length, m[2] ]; 786 | return res; 787 | }, 788 | 789 | __call__: function inline( text, patterns ) { 790 | 791 | var out = [], 792 | res; 793 | 794 | function add(x) { 795 | //D:self.debug(" adding output", uneval(x)); 796 | if ( typeof x == "string" && typeof out[out.length-1] == "string" ) 797 | out[ out.length-1 ] += x; 798 | else 799 | out.push(x); 800 | } 801 | 802 | while ( text.length > 0 ) { 803 | res = this.dialect.inline.__oneElement__.call(this, text, patterns, out ); 804 | text = text.substr( res.shift() ); 805 | forEach(res, add ) 806 | } 807 | 808 | return out; 809 | }, 810 | 811 | // These characters are intersting elsewhere, so have rules for them so that 812 | // chunks of plain text blocks don't include them 813 | "]": function () {}, 814 | "}": function () {}, 815 | 816 | "\\": function escaped( text ) { 817 | // [ length of input processed, node/children to add... ] 818 | // Only esacape: \ ` * _ { } [ ] ( ) # * + - . ! 819 | if ( text.match( /^\\[\\`\*_{}\[\]()#\+.!\-]/ ) ) 820 | return [ 2, text.charAt( 1 ) ]; 821 | else 822 | // Not an esacpe 823 | return [ 1, "\\" ]; 824 | }, 825 | 826 | "![": function image( text ) { 827 | 828 | // Unlike images, alt text is plain text only. no other elements are 829 | // allowed in there 830 | 831 | // ![Alt text](/path/to/img.jpg "Optional title") 832 | // 1 2 3 4 <--- captures 833 | var m = text.match( /^!\[(.*?)\][ \t]*\([ \t]*([^")]*?)(?:[ \t]+(["'])(.*?)\3)?[ \t]*\)/ ); 834 | 835 | if ( m ) { 836 | if ( m[2] && m[2][0] == "<" && m[2][m[2].length-1] == ">" ) 837 | m[2] = m[2].substring( 1, m[2].length - 1 ); 838 | 839 | m[2] = this.dialect.inline.__call__.call( this, m[2], /\\/ )[0]; 840 | 841 | var attrs = { alt: m[1], href: m[2] || "" }; 842 | if ( m[4] !== undefined) 843 | attrs.title = m[4]; 844 | 845 | return [ m[0].length, [ "img", attrs ] ]; 846 | } 847 | 848 | // ![Alt text][id] 849 | m = text.match( /^!\[(.*?)\][ \t]*\[(.*?)\]/ ); 850 | 851 | if ( m ) { 852 | // We can't check if the reference is known here as it likely wont be 853 | // found till after. Check it in md tree->hmtl tree conversion 854 | return [ m[0].length, [ "img_ref", { alt: m[1], ref: m[2].toLowerCase(), original: m[0] } ] ]; 855 | } 856 | 857 | // Just consume the '![' 858 | return [ 2, "![" ]; 859 | }, 860 | 861 | "[": function link( text ) { 862 | 863 | var orig = String(text); 864 | // Inline content is possible inside `link text` 865 | var res = Markdown.DialectHelpers.inline_until_char.call( this, text.substr(1), "]" ); 866 | 867 | // No closing ']' found. Just consume the [ 868 | if ( !res ) return [ 1, "[" ]; 869 | 870 | var consumed = 1 + res[ 0 ], 871 | children = res[ 1 ], 872 | link, 873 | attrs; 874 | 875 | // At this point the first [...] has been parsed. See what follows to find 876 | // out which kind of link we are (reference or direct url) 877 | text = text.substr( consumed ); 878 | 879 | // [link text](/path/to/img.jpg "Optional title") 880 | // 1 2 3 <--- captures 881 | // This will capture up to the last paren in the block. We then pull 882 | // back based on if there a matching ones in the url 883 | // ([here](/url/(test)) 884 | // The parens have to be balanced 885 | var m = text.match( /^\s*\([ \t]*([^"']*)(?:[ \t]+(["'])(.*?)\2)?[ \t]*\)/ ); 886 | if ( m ) { 887 | var url = m[1]; 888 | consumed += m[0].length; 889 | 890 | if ( url && url[0] == "<" && url[url.length-1] == ">" ) 891 | url = url.substring( 1, url.length - 1 ); 892 | 893 | // If there is a title we don't have to worry about parens in the url 894 | if ( !m[3] ) { 895 | var open_parens = 1; // One open that isn't in the capture 896 | for ( var len = 0; len < url.length; len++ ) { 897 | switch ( url[len] ) { 898 | case "(": 899 | open_parens++; 900 | break; 901 | case ")": 902 | if ( --open_parens == 0) { 903 | consumed -= url.length - len; 904 | url = url.substring(0, len); 905 | } 906 | break; 907 | } 908 | } 909 | } 910 | 911 | // Process escapes only 912 | url = this.dialect.inline.__call__.call( this, url, /\\/ )[0]; 913 | 914 | attrs = { href: url || "" }; 915 | if ( m[3] !== undefined) 916 | attrs.title = m[3]; 917 | 918 | link = [ "link", attrs ].concat( children ); 919 | return [ consumed, link ]; 920 | } 921 | 922 | // [Alt text][id] 923 | // [Alt text] [id] 924 | m = text.match( /^\s*\[(.*?)\]/ ); 925 | 926 | if ( m ) { 927 | 928 | consumed += m[ 0 ].length; 929 | 930 | // [links][] uses links as its reference 931 | attrs = { ref: ( m[ 1 ] || String(children) ).toLowerCase(), original: orig.substr( 0, consumed ) }; 932 | 933 | link = [ "link_ref", attrs ].concat( children ); 934 | 935 | // We can't check if the reference is known here as it likely wont be 936 | // found till after. Check it in md tree->hmtl tree conversion. 937 | // Store the original so that conversion can revert if the ref isn't found. 938 | return [ consumed, link ]; 939 | } 940 | 941 | // [id] 942 | // Only if id is plain (no formatting.) 943 | if ( children.length == 1 && typeof children[0] == "string" ) { 944 | 945 | attrs = { ref: children[0].toLowerCase(), original: orig.substr( 0, consumed ) }; 946 | link = [ "link_ref", attrs, children[0] ]; 947 | return [ consumed, link ]; 948 | } 949 | 950 | // Just consume the "[" 951 | return [ 1, "[" ]; 952 | }, 953 | 954 | 955 | "<": function autoLink( text ) { 956 | var m; 957 | 958 | if ( ( m = text.match( /^<(?:((https?|ftp|mailto):[^>]+)|(.*?@.*?\.[a-zA-Z]+))>/ ) ) != null ) { 959 | if ( m[3] ) { 960 | return [ m[0].length, [ "link", { href: "mailto:" + m[3] }, m[3] ] ]; 961 | 962 | } 963 | else if ( m[2] == "mailto" ) { 964 | return [ m[0].length, [ "link", { href: m[1] }, m[1].substr("mailto:".length ) ] ]; 965 | } 966 | else 967 | return [ m[0].length, [ "link", { href: m[1] }, m[1] ] ]; 968 | } 969 | 970 | return [ 1, "<" ]; 971 | }, 972 | 973 | "`": function inlineCode( text ) { 974 | // Inline code block. as many backticks as you like to start it 975 | // Always skip over the opening ticks. 976 | var m = text.match( /(`+)(([\s\S]*?)\1)/ ); 977 | 978 | if ( m && m[2] ) 979 | return [ m[1].length + m[2].length, [ "inlinecode", m[3] ] ]; 980 | else { 981 | // TODO: No matching end code found - warn! 982 | return [ 1, "`" ]; 983 | } 984 | }, 985 | 986 | " \n": function lineBreak( text ) { 987 | return [ 3, [ "linebreak" ] ]; 988 | } 989 | 990 | }; 991 | 992 | // Meta Helper/generator method for em and strong handling 993 | function strong_em( tag, md ) { 994 | 995 | var state_slot = tag + "_state", 996 | other_slot = tag == "strong" ? "em_state" : "strong_state"; 997 | 998 | function CloseTag(len) { 999 | this.len_after = len; 1000 | this.name = "close_" + md; 1001 | } 1002 | 1003 | return function ( text, orig_match ) { 1004 | 1005 | if ( this[state_slot][0] == md ) { 1006 | // Most recent em is of this type 1007 | //D:this.debug("closing", md); 1008 | this[state_slot].shift(); 1009 | 1010 | // "Consume" everything to go back to the recrusion in the else-block below 1011 | return[ text.length, new CloseTag(text.length-md.length) ]; 1012 | } 1013 | else { 1014 | // Store a clone of the em/strong states 1015 | var other = this[other_slot].slice(), 1016 | state = this[state_slot].slice(); 1017 | 1018 | this[state_slot].unshift(md); 1019 | 1020 | //D:this.debug_indent += " "; 1021 | 1022 | // Recurse 1023 | var res = this.processInline( text.substr( md.length ) ); 1024 | //D:this.debug_indent = this.debug_indent.substr(2); 1025 | 1026 | var last = res[res.length - 1]; 1027 | 1028 | //D:this.debug("processInline from", tag + ": ", uneval( res ) ); 1029 | 1030 | var check = this[state_slot].shift(); 1031 | if ( last instanceof CloseTag ) { 1032 | res.pop(); 1033 | // We matched! Huzzah. 1034 | var consumed = text.length - last.len_after; 1035 | return [ consumed, [ tag ].concat(res) ]; 1036 | } 1037 | else { 1038 | // Restore the state of the other kind. We might have mistakenly closed it. 1039 | this[other_slot] = other; 1040 | this[state_slot] = state; 1041 | 1042 | // We can't reuse the processed result as it could have wrong parsing contexts in it. 1043 | return [ md.length, md ]; 1044 | } 1045 | } 1046 | }; // End returned function 1047 | } 1048 | 1049 | Markdown.dialects.Gruber.inline["**"] = strong_em("strong", "**"); 1050 | Markdown.dialects.Gruber.inline["__"] = strong_em("strong", "__"); 1051 | Markdown.dialects.Gruber.inline["*"] = strong_em("em", "*"); 1052 | Markdown.dialects.Gruber.inline["_"] = strong_em("em", "_"); 1053 | 1054 | 1055 | // Build default order from insertion order. 1056 | Markdown.buildBlockOrder = function(d) { 1057 | var ord = []; 1058 | for ( var i in d ) { 1059 | if ( i == "__order__" || i == "__call__" ) continue; 1060 | ord.push( i ); 1061 | } 1062 | d.__order__ = ord; 1063 | }; 1064 | 1065 | // Build patterns for inline matcher 1066 | Markdown.buildInlinePatterns = function(d) { 1067 | var patterns = []; 1068 | 1069 | for ( var i in d ) { 1070 | // __foo__ is reserved and not a pattern 1071 | if ( i.match( /^__.*__$/) ) continue; 1072 | var l = i.replace( /([\\.*+?|()\[\]{}])/g, "\\$1" ) 1073 | .replace( /\n/, "\\n" ); 1074 | patterns.push( i.length == 1 ? l : "(?:" + l + ")" ); 1075 | } 1076 | 1077 | patterns = patterns.join("|"); 1078 | d.__patterns__ = patterns; 1079 | //print("patterns:", uneval( patterns ) ); 1080 | 1081 | var fn = d.__call__; 1082 | d.__call__ = function(text, pattern) { 1083 | if ( pattern != undefined ) { 1084 | return fn.call(this, text, pattern); 1085 | } 1086 | else 1087 | { 1088 | return fn.call(this, text, patterns); 1089 | } 1090 | }; 1091 | }; 1092 | 1093 | Markdown.DialectHelpers = {}; 1094 | Markdown.DialectHelpers.inline_until_char = function( text, want ) { 1095 | var consumed = 0, 1096 | nodes = []; 1097 | 1098 | while ( true ) { 1099 | if ( text.charAt( consumed ) == want ) { 1100 | // Found the character we were looking for 1101 | consumed++; 1102 | return [ consumed, nodes ]; 1103 | } 1104 | 1105 | if ( consumed >= text.length ) { 1106 | // No closing char found. Abort. 1107 | return null; 1108 | } 1109 | 1110 | var res = this.dialect.inline.__oneElement__.call(this, text.substr( consumed ) ); 1111 | consumed += res[ 0 ]; 1112 | // Add any returned nodes. 1113 | nodes.push.apply( nodes, res.slice( 1 ) ); 1114 | } 1115 | } 1116 | 1117 | // Helper function to make sub-classing a dialect easier 1118 | Markdown.subclassDialect = function( d ) { 1119 | function Block() {} 1120 | Block.prototype = d.block; 1121 | function Inline() {} 1122 | Inline.prototype = d.inline; 1123 | 1124 | return { block: new Block(), inline: new Inline() }; 1125 | }; 1126 | 1127 | Markdown.buildBlockOrder ( Markdown.dialects.Gruber.block ); 1128 | Markdown.buildInlinePatterns( Markdown.dialects.Gruber.inline ); 1129 | 1130 | Markdown.dialects.Maruku = Markdown.subclassDialect( Markdown.dialects.Gruber ); 1131 | 1132 | Markdown.dialects.Maruku.processMetaHash = function processMetaHash( meta_string ) { 1133 | var meta = split_meta_hash( meta_string ), 1134 | attr = {}; 1135 | 1136 | for ( var i = 0; i < meta.length; ++i ) { 1137 | // id: #foo 1138 | if ( /^#/.test( meta[ i ] ) ) { 1139 | attr.id = meta[ i ].substring( 1 ); 1140 | } 1141 | // class: .foo 1142 | else if ( /^\./.test( meta[ i ] ) ) { 1143 | // if class already exists, append the new one 1144 | if ( attr["class"] ) { 1145 | attr["class"] = attr["class"] + meta[ i ].replace( /./, " " ); 1146 | } 1147 | else { 1148 | attr["class"] = meta[ i ].substring( 1 ); 1149 | } 1150 | } 1151 | // attribute: foo=bar 1152 | else if ( /\=/.test( meta[ i ] ) ) { 1153 | var s = meta[ i ].split( /\=/ ); 1154 | attr[ s[ 0 ] ] = s[ 1 ]; 1155 | } 1156 | } 1157 | 1158 | return attr; 1159 | } 1160 | 1161 | function split_meta_hash( meta_string ) { 1162 | var meta = meta_string.split( "" ), 1163 | parts = [ "" ], 1164 | in_quotes = false; 1165 | 1166 | while ( meta.length ) { 1167 | var letter = meta.shift(); 1168 | switch ( letter ) { 1169 | case " " : 1170 | // if we're in a quoted section, keep it 1171 | if ( in_quotes ) { 1172 | parts[ parts.length - 1 ] += letter; 1173 | } 1174 | // otherwise make a new part 1175 | else { 1176 | parts.push( "" ); 1177 | } 1178 | break; 1179 | case "'" : 1180 | case '"' : 1181 | // reverse the quotes and move straight on 1182 | in_quotes = !in_quotes; 1183 | break; 1184 | case "\\" : 1185 | // shift off the next letter to be used straight away. 1186 | // it was escaped so we'll keep it whatever it is 1187 | letter = meta.shift(); 1188 | default : 1189 | parts[ parts.length - 1 ] += letter; 1190 | break; 1191 | } 1192 | } 1193 | 1194 | return parts; 1195 | } 1196 | 1197 | Markdown.dialects.Maruku.block.document_meta = function document_meta( block, next ) { 1198 | // we're only interested in the first block 1199 | if ( block.lineNumber > 1 ) return undefined; 1200 | 1201 | // document_meta blocks consist of one or more lines of `Key: Value\n` 1202 | if ( ! block.match( /^(?:\w+:.*\n)*\w+:.*$/ ) ) return undefined; 1203 | 1204 | // make an attribute node if it doesn't exist 1205 | if ( !extract_attr( this.tree ) ) { 1206 | this.tree.splice( 1, 0, {} ); 1207 | } 1208 | 1209 | var pairs = block.split( /\n/ ); 1210 | for ( p in pairs ) { 1211 | var m = pairs[ p ].match( /(\w+):\s*(.*)$/ ), 1212 | key = m[ 1 ].toLowerCase(), 1213 | value = m[ 2 ]; 1214 | 1215 | this.tree[ 1 ][ key ] = value; 1216 | } 1217 | 1218 | // document_meta produces no content! 1219 | return []; 1220 | }; 1221 | 1222 | Markdown.dialects.Maruku.block.block_meta = function block_meta( block, next ) { 1223 | // check if the last line of the block is an meta hash 1224 | var m = block.match( /(^|\n) {0,3}\{:\s*((?:\\\}|[^\}])*)\s*\}$/ ); 1225 | if ( !m ) return undefined; 1226 | 1227 | // process the meta hash 1228 | var attr = this.dialect.processMetaHash( m[ 2 ] ); 1229 | 1230 | var hash; 1231 | 1232 | // if we matched ^ then we need to apply meta to the previous block 1233 | if ( m[ 1 ] === "" ) { 1234 | var node = this.tree[ this.tree.length - 1 ]; 1235 | hash = extract_attr( node ); 1236 | 1237 | // if the node is a string (rather than JsonML), bail 1238 | if ( typeof node === "string" ) return undefined; 1239 | 1240 | // create the attribute hash if it doesn't exist 1241 | if ( !hash ) { 1242 | hash = {}; 1243 | node.splice( 1, 0, hash ); 1244 | } 1245 | 1246 | // add the attributes in 1247 | for ( a in attr ) { 1248 | hash[ a ] = attr[ a ]; 1249 | } 1250 | 1251 | // return nothing so the meta hash is removed 1252 | return []; 1253 | } 1254 | 1255 | // pull the meta hash off the block and process what's left 1256 | var b = block.replace( /\n.*$/, "" ), 1257 | result = this.processBlock( b, [] ); 1258 | 1259 | // get or make the attributes hash 1260 | hash = extract_attr( result[ 0 ] ); 1261 | if ( !hash ) { 1262 | hash = {}; 1263 | result[ 0 ].splice( 1, 0, hash ); 1264 | } 1265 | 1266 | // attach the attributes to the block 1267 | for ( a in attr ) { 1268 | hash[ a ] = attr[ a ]; 1269 | } 1270 | 1271 | return result; 1272 | }; 1273 | 1274 | Markdown.dialects.Maruku.block.definition_list = function definition_list( block, next ) { 1275 | // one or more terms followed by one or more definitions, in a single block 1276 | var tight = /^((?:[^\s:].*\n)+):\s+([\s\S]+)$/, 1277 | list = [ "dl" ], 1278 | i; 1279 | 1280 | // see if we're dealing with a tight or loose block 1281 | if ( ( m = block.match( tight ) ) ) { 1282 | // pull subsequent tight DL blocks out of `next` 1283 | var blocks = [ block ]; 1284 | while ( next.length && tight.exec( next[ 0 ] ) ) { 1285 | blocks.push( next.shift() ); 1286 | } 1287 | 1288 | for ( var b = 0; b < blocks.length; ++b ) { 1289 | var m = blocks[ b ].match( tight ), 1290 | terms = m[ 1 ].replace( /\n$/, "" ).split( /\n/ ), 1291 | defns = m[ 2 ].split( /\n:\s+/ ); 1292 | 1293 | // print( uneval( m ) ); 1294 | 1295 | for ( i = 0; i < terms.length; ++i ) { 1296 | list.push( [ "dt", terms[ i ] ] ); 1297 | } 1298 | 1299 | for ( i = 0; i < defns.length; ++i ) { 1300 | // run inline processing over the definition 1301 | list.push( [ "dd" ].concat( this.processInline( defns[ i ].replace( /(\n)\s+/, "$1" ) ) ) ); 1302 | } 1303 | } 1304 | } 1305 | else { 1306 | return undefined; 1307 | } 1308 | 1309 | return [ list ]; 1310 | }; 1311 | 1312 | Markdown.dialects.Maruku.inline[ "{:" ] = function inline_meta( text, matches, out ) { 1313 | if ( !out.length ) { 1314 | return [ 2, "{:" ]; 1315 | } 1316 | 1317 | // get the preceeding element 1318 | var before = out[ out.length - 1 ]; 1319 | 1320 | if ( typeof before === "string" ) { 1321 | return [ 2, "{:" ]; 1322 | } 1323 | 1324 | // match a meta hash 1325 | var m = text.match( /^\{:\s*((?:\\\}|[^\}])*)\s*\}/ ); 1326 | 1327 | // no match, false alarm 1328 | if ( !m ) { 1329 | return [ 2, "{:" ]; 1330 | } 1331 | 1332 | // attach the attributes to the preceeding element 1333 | var meta = this.dialect.processMetaHash( m[ 1 ] ), 1334 | attr = extract_attr( before ); 1335 | 1336 | if ( !attr ) { 1337 | attr = {}; 1338 | before.splice( 1, 0, attr ); 1339 | } 1340 | 1341 | for ( var k in meta ) { 1342 | attr[ k ] = meta[ k ]; 1343 | } 1344 | 1345 | // cut out the string and replace it with nothing 1346 | return [ m[ 0 ].length, "" ]; 1347 | }; 1348 | 1349 | Markdown.buildBlockOrder ( Markdown.dialects.Maruku.block ); 1350 | Markdown.buildInlinePatterns( Markdown.dialects.Maruku.inline ); 1351 | 1352 | var isArray = Array.isArray || function(obj) { 1353 | return Object.prototype.toString.call(obj) == "[object Array]"; 1354 | }; 1355 | 1356 | var forEach; 1357 | // Don't mess with Array.prototype. Its not friendly 1358 | if ( Array.prototype.forEach ) { 1359 | forEach = function( arr, cb, thisp ) { 1360 | return arr.forEach( cb, thisp ); 1361 | }; 1362 | } 1363 | else { 1364 | forEach = function(arr, cb, thisp) { 1365 | for (var i = 0; i < arr.length; i++) { 1366 | cb.call(thisp || arr, arr[i], i, arr); 1367 | } 1368 | } 1369 | } 1370 | 1371 | var isEmpty = function( obj ) { 1372 | for ( var key in obj ) { 1373 | if ( hasOwnProperty.call( obj, key ) ) { 1374 | return false; 1375 | } 1376 | } 1377 | 1378 | return true; 1379 | } 1380 | 1381 | function extract_attr( jsonml ) { 1382 | return isArray(jsonml) 1383 | && jsonml.length > 1 1384 | && typeof jsonml[ 1 ] === "object" 1385 | && !( isArray(jsonml[ 1 ]) ) 1386 | ? jsonml[ 1 ] 1387 | : undefined; 1388 | } 1389 | 1390 | 1391 | 1392 | /** 1393 | * renderJsonML( jsonml[, options] ) -> String 1394 | * - jsonml (Array): JsonML array to render to XML 1395 | * - options (Object): options 1396 | * 1397 | * Converts the given JsonML into well-formed XML. 1398 | * 1399 | * The options currently understood are: 1400 | * 1401 | * - root (Boolean): wether or not the root node should be included in the 1402 | * output, or just its children. The default `false` is to not include the 1403 | * root itself. 1404 | */ 1405 | expose.renderJsonML = function( jsonml, options ) { 1406 | options = options || {}; 1407 | // include the root element in the rendered output? 1408 | options.root = options.root || false; 1409 | 1410 | var content = []; 1411 | 1412 | if ( options.root ) { 1413 | content.push( render_tree( jsonml ) ); 1414 | } 1415 | else { 1416 | jsonml.shift(); // get rid of the tag 1417 | if ( jsonml.length && typeof jsonml[ 0 ] === "object" && !( jsonml[ 0 ] instanceof Array ) ) { 1418 | jsonml.shift(); // get rid of the attributes 1419 | } 1420 | 1421 | while ( jsonml.length ) { 1422 | content.push( render_tree( jsonml.shift() ) ); 1423 | } 1424 | } 1425 | 1426 | return content.join( "\n\n" ); 1427 | }; 1428 | 1429 | function escapeHTML( text ) { 1430 | return text.replace( /&/g, "&" ) 1431 | .replace( //g, ">" ) 1433 | .replace( /"/g, """ ) 1434 | .replace( /'/g, "'" ); 1435 | } 1436 | 1437 | function render_tree( jsonml ) { 1438 | // basic case 1439 | if ( typeof jsonml === "string" ) { 1440 | return escapeHTML( jsonml ); 1441 | } 1442 | 1443 | var tag = jsonml.shift(), 1444 | attributes = {}, 1445 | content = []; 1446 | 1447 | if ( jsonml.length && typeof jsonml[ 0 ] === "object" && !( jsonml[ 0 ] instanceof Array ) ) { 1448 | attributes = jsonml.shift(); 1449 | } 1450 | 1451 | while ( jsonml.length ) { 1452 | content.push( render_tree( jsonml.shift() ) ); 1453 | } 1454 | 1455 | var tag_attrs = ""; 1456 | for ( var a in attributes ) { 1457 | tag_attrs += " " + a + '="' + escapeHTML( attributes[ a ] ) + '"'; 1458 | } 1459 | 1460 | // be careful about adding whitespace here for inline elements 1461 | if ( tag == "img" || tag == "br" || tag == "hr" ) { 1462 | return "<"+ tag + tag_attrs + "/>"; 1463 | } 1464 | else { 1465 | return "<"+ tag + tag_attrs + ">" + content.join( "" ) + ""; 1466 | } 1467 | } 1468 | 1469 | function convert_tree_to_html( tree, references, options ) { 1470 | var i; 1471 | options = options || {}; 1472 | 1473 | // shallow clone 1474 | var jsonml = tree.slice( 0 ); 1475 | 1476 | if ( typeof options.preprocessTreeNode === "function" ) { 1477 | jsonml = options.preprocessTreeNode(jsonml, references); 1478 | } 1479 | 1480 | // Clone attributes if they exist 1481 | var attrs = extract_attr( jsonml ); 1482 | if ( attrs ) { 1483 | jsonml[ 1 ] = {}; 1484 | for ( i in attrs ) { 1485 | jsonml[ 1 ][ i ] = attrs[ i ]; 1486 | } 1487 | attrs = jsonml[ 1 ]; 1488 | } 1489 | 1490 | // basic case 1491 | if ( typeof jsonml === "string" ) { 1492 | return jsonml; 1493 | } 1494 | 1495 | // convert this node 1496 | switch ( jsonml[ 0 ] ) { 1497 | case "header": 1498 | jsonml[ 0 ] = "h" + jsonml[ 1 ].level; 1499 | delete jsonml[ 1 ].level; 1500 | break; 1501 | case "bulletlist": 1502 | jsonml[ 0 ] = "ul"; 1503 | break; 1504 | case "numberlist": 1505 | jsonml[ 0 ] = "ol"; 1506 | break; 1507 | case "listitem": 1508 | jsonml[ 0 ] = "li"; 1509 | break; 1510 | case "para": 1511 | jsonml[ 0 ] = "p"; 1512 | break; 1513 | case "markdown": 1514 | jsonml[ 0 ] = "html"; 1515 | if ( attrs ) delete attrs.references; 1516 | break; 1517 | case "code_block": 1518 | jsonml[ 0 ] = "pre"; 1519 | i = attrs ? 2 : 1; 1520 | var code = [ "code" ]; 1521 | code.push.apply( code, jsonml.splice( i, jsonml.length - i ) ); 1522 | jsonml[ i ] = code; 1523 | break; 1524 | case "inlinecode": 1525 | jsonml[ 0 ] = "code"; 1526 | break; 1527 | case "img": 1528 | jsonml[ 1 ].src = jsonml[ 1 ].href; 1529 | delete jsonml[ 1 ].href; 1530 | break; 1531 | case "linebreak": 1532 | jsonml[ 0 ] = "br"; 1533 | break; 1534 | case "link": 1535 | jsonml[ 0 ] = "a"; 1536 | break; 1537 | case "link_ref": 1538 | jsonml[ 0 ] = "a"; 1539 | 1540 | // grab this ref and clean up the attribute node 1541 | var ref = references[ attrs.ref ]; 1542 | 1543 | // if the reference exists, make the link 1544 | if ( ref ) { 1545 | delete attrs.ref; 1546 | 1547 | // add in the href and title, if present 1548 | attrs.href = ref.href; 1549 | if ( ref.title ) { 1550 | attrs.title = ref.title; 1551 | } 1552 | 1553 | // get rid of the unneeded original text 1554 | delete attrs.original; 1555 | } 1556 | // the reference doesn't exist, so revert to plain text 1557 | else { 1558 | return attrs.original; 1559 | } 1560 | break; 1561 | case "img_ref": 1562 | jsonml[ 0 ] = "img"; 1563 | 1564 | // grab this ref and clean up the attribute node 1565 | var ref = references[ attrs.ref ]; 1566 | 1567 | // if the reference exists, make the link 1568 | if ( ref ) { 1569 | delete attrs.ref; 1570 | 1571 | // add in the href and title, if present 1572 | attrs.src = ref.href; 1573 | if ( ref.title ) { 1574 | attrs.title = ref.title; 1575 | } 1576 | 1577 | // get rid of the unneeded original text 1578 | delete attrs.original; 1579 | } 1580 | // the reference doesn't exist, so revert to plain text 1581 | else { 1582 | return attrs.original; 1583 | } 1584 | break; 1585 | } 1586 | 1587 | // convert all the children 1588 | i = 1; 1589 | 1590 | // deal with the attribute node, if it exists 1591 | if ( attrs ) { 1592 | // if there are keys, skip over it 1593 | for ( var key in jsonml[ 1 ] ) { 1594 | i = 2; 1595 | } 1596 | // if there aren't, remove it 1597 | if ( i === 1 ) { 1598 | jsonml.splice( i, 1 ); 1599 | } 1600 | } 1601 | 1602 | for ( ; i < jsonml.length; ++i ) { 1603 | jsonml[ i ] = convert_tree_to_html( jsonml[ i ], references, options ); 1604 | } 1605 | 1606 | return jsonml; 1607 | } 1608 | 1609 | 1610 | // merges adjacent text nodes into a single node 1611 | function merge_text_nodes( jsonml ) { 1612 | // skip the tag name and attribute hash 1613 | var i = extract_attr( jsonml ) ? 2 : 1; 1614 | 1615 | while ( i < jsonml.length ) { 1616 | // if it's a string check the next item too 1617 | if ( typeof jsonml[ i ] === "string" ) { 1618 | if ( i + 1 < jsonml.length && typeof jsonml[ i + 1 ] === "string" ) { 1619 | // merge the second string into the first and remove it 1620 | jsonml[ i ] += jsonml.splice( i + 1, 1 )[ 0 ]; 1621 | } 1622 | else { 1623 | ++i; 1624 | } 1625 | } 1626 | // if it's not a string recurse 1627 | else { 1628 | merge_text_nodes( jsonml[ i ] ); 1629 | ++i; 1630 | } 1631 | } 1632 | } 1633 | 1634 | } )( (function() { 1635 | if ( typeof exports === "undefined" ) { 1636 | window.markdown = {}; 1637 | return window.markdown; 1638 | } 1639 | else { 1640 | return exports; 1641 | } 1642 | } )() ); 1643 | -------------------------------------------------------------------------------- /style.example.css: -------------------------------------------------------------------------------- 1 | * { 2 | padding: 5pt; 3 | margin: 0; 4 | border: 0; 5 | font-size: 12pt; 6 | } --------------------------------------------------------------------------------