├── .npmrc ├── .eslintignore ├── assets └── demo.png ├── src ├── util.ts ├── extendPrism.ts ├── main.ts ├── postProcessor.ts └── CM6Extensions.ts ├── versions.json ├── .editorconfig ├── manifest.json ├── .gitignore ├── tsconfig.json ├── version-bump.mjs ├── .eslintrc ├── LICENSE ├── package.json ├── esbuild.config.mjs ├── README.md ├── prism-line-highlight.css ├── styles.css └── lib ├── prism-line-numbers.js └── prism-line-highlight.js /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | npm node_modules 2 | build -------------------------------------------------------------------------------- /assets/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lijyze/obsidian-advanced-codeblock/HEAD/assets/demo.png -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | export const paramRegex = /\{.+\}|\w+/g; 2 | export const braceSurroundingRegex = /^{.+}$/; -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.1": "0.12.0", 3 | "1.0.2": "0.12.0", 4 | "1.0.3": "0.12.0", 5 | "1.1.0": "0.12.0", 6 | "1.1.1": "0.12.0", 7 | "1.1.2": "0.12.0", 8 | "1.1.4": "0.12.0", 9 | "1.1.5": "0.12.0" 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 4 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-advanced-codeblock", 3 | "name": "Advanced Codeblock", 4 | "version": "1.1.5", 5 | "minAppVersion": "0.12.0", 6 | "description": "Give additioinal features to code blocks.", 7 | "author": "Lijyze", 8 | "authorUrl": "https://github.com/lijyze", 9 | "isDesktopOnly": false 10 | } 11 | -------------------------------------------------------------------------------- /src/extendPrism.ts: -------------------------------------------------------------------------------- 1 | import { loadPrism } from 'obsidian'; 2 | import { extendLineNumberPlugin } from './../lib/prism-line-numbers'; 3 | import { extendLineHighlightPlugin } from './../lib/prism-line-highlight' 4 | 5 | loadPrism().then((val) => { 6 | extendLineNumberPlugin(window.Prism) 7 | extendLineHighlightPlugin(window.Prism) 8 | }); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | 15 | # Exclude sourcemaps 16 | *.map 17 | 18 | # obsidian 19 | data.json 20 | 21 | # Exclude macOS Finder (System Explorer) View States 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES6", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "isolatedModules": true, 13 | "lib": [ 14 | "DOM", 15 | "ES5", 16 | "ES6", 17 | "ES7" 18 | ] 19 | }, 20 | "include": [ 21 | "**/*.ts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from "obsidian"; 2 | import { commonCodeblockPostProcessor } from "./postProcessor"; 3 | import { livePreviewCM6Extension } from './CM6Extensions'; 4 | 5 | export default class ObsidianAdvancedCodeblockPlugin extends Plugin { 6 | async onload() { 7 | console.log("Loading Advanced Codeblock"); 8 | 9 | // Add functionality to live-preview mode 10 | this.registerEditorExtension([livePreviewCM6Extension]); 11 | 12 | // Add functionality to preview mode 13 | this.registerMarkdownPostProcessor((element, context) => { 14 | commonCodeblockPostProcessor(element, context, this.app, this); 15 | }); 16 | } 17 | 18 | onunload() {} 19 | } 20 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = process.env.npm_package_version; 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 7 | const { minAppVersion } = manifest; 8 | manifest.version = targetVersion; 9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 13 | versions[targetVersion] = minAppVersion; 14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "no-prototype-builtins": "off", 21 | "@typescript-eslint/no-empty-function": "off" 22 | } 23 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 lijyze 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-sample-plugin", 3 | "version": "1.0.1", 4 | "description": "This is a sample plugin for Obsidian (https://obsidian.md)", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "version": "node version-bump.mjs && git add manifest.json versions.json" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@types/node": "^16.11.6", 16 | "@types/prismjs": "^1.26.0", 17 | "@typescript-eslint/eslint-plugin": "^5.2.0", 18 | "@typescript-eslint/parser": "^5.2.0", 19 | "@types/codemirror": "0.0.108", 20 | "moment": "2.29.4", 21 | "@codemirror/language": "^0.20.0", 22 | "@codemirror/stream-parser": "https://github.com/lishid/stream-parser", 23 | "builtin-modules": "^3.2.0", 24 | "esbuild": "0.13.12", 25 | "obsidian": "latest", 26 | "tslib": "2.3.1", 27 | "typescript": "4.4.4" 28 | }, 29 | "peerDependencies": { 30 | "@codemirror/state": "^6.0.0", 31 | "@codemirror/view": "^6.0.0", 32 | "@codemirror/language": "^6.0.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from 'builtin-modules' 4 | 5 | const banner = 6 | `/* 7 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 8 | if you want to view the source, please visit the github repository of this plugin 9 | */ 10 | `; 11 | 12 | const prod = (process.argv[2] === 'production'); 13 | 14 | esbuild.build({ 15 | banner: { 16 | js: banner, 17 | }, 18 | entryPoints: ['src/main.ts'], 19 | bundle: true, 20 | external: [ 21 | 'obsidian', 22 | 'electron', 23 | '@codemirror/autocomplete', 24 | '@codemirror/closebrackets', 25 | '@codemirror/collab', 26 | '@codemirror/commands', 27 | '@codemirror/comment', 28 | '@codemirror/fold', 29 | '@codemirror/gutter', 30 | '@codemirror/highlight', 31 | '@codemirror/history', 32 | '@codemirror/language', 33 | '@codemirror/lint', 34 | '@codemirror/matchbrackets', 35 | '@codemirror/panel', 36 | '@codemirror/rangeset', 37 | '@codemirror/rectangular-selection', 38 | '@codemirror/search', 39 | '@codemirror/state', 40 | '@codemirror/stream-parser', 41 | '@codemirror/text', 42 | '@codemirror/tooltip', 43 | '@codemirror/view', 44 | ...builtins], 45 | format: 'cjs', 46 | watch: !prod, 47 | target: 'es2016', 48 | logLevel: "info", 49 | sourcemap: prod ? false : 'inline', 50 | treeShaking: true, 51 | outfile: 'main.js', 52 | }).catch(() => process.exit(1)); 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Obsidian Advanced Codeblock 2 | 3 | Add additioinal features to code blocks. 4 | 5 | ## Demo 6 | 7 | ![demo](https://raw.githubusercontent.com/lijyze/obsidian-advanced-codeblock/main/assets/demo.png) 8 | 9 | ## Feature 10 | 11 | 1. Add line numbers to code block 12 | 2. Add line highlight to code block 13 | 14 | ## Usage 15 | 16 | All features won't apply universally, if you need any feature, you need to correctly specify params to specific code block. Params should be split by `' '`. 17 | 18 | | Feature | param | description | 19 | | ----------------- | -------- | --------------- | 20 | | show line numbers | nums | 21 | | highlight line | {a, b-c} | brackets matter | 22 | 23 | ## Notice 24 | 25 | - **Code block won't update in preview mode if you only change params of blocks.** 26 | 27 | Obsidian cache every section of artical, but it directly ignore anything follows the first space after ```` ```language ````. Which means if you only change params of a code block, obsidian will think nothing has been changed, so it won't call any post processor in preview view render process. 28 | 29 | ## Manually installing the plugin 30 | 31 | - Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/obsidian-advanced-codeblock/`. 32 | 33 | ## Releases 34 | 35 | ### 1.1.0 36 | 37 | 1. Funcitonalities are available in live-preview mode! 38 | 39 | ## Donating 40 | 41 | Buy Me A Coffee 42 | -------------------------------------------------------------------------------- /prism-line-highlight.css: -------------------------------------------------------------------------------- 1 | pre[data-line] { 2 | position: relative; 3 | padding: 1em 0 1em 3em; 4 | } 5 | 6 | .line-highlight { 7 | position: absolute; 8 | left: 0; 9 | right: 0; 10 | padding: inherit 0; 11 | margin-top: 1em; /* Same as .prism’s padding-top */ 12 | 13 | background: hsla(24, 20%, 50%,.08); 14 | background: linear-gradient(to right, hsla(24, 20%, 50%,.1) 70%, hsla(24, 20%, 50%,0)); 15 | 16 | pointer-events: none; 17 | 18 | line-height: inherit; 19 | white-space: pre; 20 | } 21 | 22 | @media print { 23 | .line-highlight { 24 | /* 25 | * This will prevent browsers from replacing the background color with white. 26 | * It's necessary because the element is layered on top of the displayed code. 27 | */ 28 | -webkit-print-color-adjust: exact; 29 | color-adjust: exact; 30 | } 31 | } 32 | 33 | .line-highlight:before, 34 | .line-highlight[data-end]:after { 35 | content: attr(data-start); 36 | position: absolute; 37 | top: .4em; 38 | left: .6em; 39 | min-width: 1em; 40 | padding: 0 .5em; 41 | background-color: hsla(24, 20%, 50%,.4); 42 | color: hsl(24, 20%, 95%); 43 | font: bold 65%/1.5 sans-serif; 44 | text-align: center; 45 | vertical-align: .3em; 46 | border-radius: 999px; 47 | text-shadow: none; 48 | box-shadow: 0 1px white; 49 | } 50 | 51 | .line-highlight[data-end]:after { 52 | content: attr(data-end); 53 | top: auto; 54 | bottom: .4em; 55 | } 56 | 57 | .line-numbers .line-highlight:before, 58 | .line-numbers .line-highlight:after { 59 | content: none; 60 | } 61 | 62 | pre[id].linkable-line-numbers span.line-numbers-rows { 63 | pointer-events: all; 64 | } 65 | pre[id].linkable-line-numbers span.line-numbers-rows > span:before { 66 | cursor: pointer; 67 | } 68 | pre[id].linkable-line-numbers span.line-numbers-rows > span:hover:before { 69 | background-color: rgba(128, 128, 128, .2); 70 | } 71 | -------------------------------------------------------------------------------- /src/postProcessor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | App, 3 | MarkdownPostProcessorContext, 4 | MarkdownView, 5 | Plugin, 6 | } from "obsidian"; 7 | import { paramRegex, braceSurroundingRegex } from "./util"; 8 | import "./extendPrism"; 9 | 10 | type HandlerSet = (() => void)[]; 11 | 12 | function processParams( 13 | element: HTMLElement, 14 | context: MarkdownPostProcessorContext, 15 | app: App 16 | ) { 17 | // Only works with code blocks; 18 | const pre: HTMLPreElement = element.querySelector("pre:not(.frontmatter)"); 19 | if (!pre) return null; 20 | 21 | const codeBlock = context.getSectionInfo(element); 22 | if (!codeBlock) return null; 23 | 24 | const origin = app.workspace 25 | .getActiveViewOfType(MarkdownView) 26 | ?.editor.getLine(codeBlock.lineStart) 27 | .slice(3); 28 | 29 | // not specify anything or no active view 30 | if (!origin) return null; 31 | 32 | const codeBlockInfo = origin.match(paramRegex); 33 | const params = codeBlockInfo.slice(1); 34 | 35 | // return if no param 36 | if (!params.length) return null; 37 | 38 | return { pre, params }; 39 | } 40 | 41 | function onMounted(element: HTMLElement, onAttachCallback: () => void) { 42 | const observer = new MutationObserver(function () { 43 | function isAttached(el: HTMLElement | null): boolean { 44 | if (el.parentNode === document) { 45 | return true; 46 | } else if (el.parentNode === null) { 47 | return false; 48 | } else { 49 | return isAttached(el.parentNode as HTMLElement); 50 | } 51 | } 52 | 53 | if (isAttached(element)) { 54 | observer.disconnect(); 55 | onAttachCallback(); 56 | } 57 | }); 58 | 59 | observer.observe(document, { 60 | childList: true, 61 | subtree: true, 62 | }); 63 | } 64 | 65 | // handle line number 66 | function handleLineNumbers( 67 | pre: HTMLPreElement, 68 | params: string[], 69 | initHandlers: HandlerSet, 70 | ) { 71 | if (!params.includes("nums")) return; 72 | 73 | pre.classList.add("line-numbers"); 74 | 75 | const initLineNumbers = () => { 76 | window.Prism.plugins.lineNumbers.resize(pre); 77 | } 78 | 79 | initHandlers.push(initLineNumbers); 80 | } 81 | 82 | function handleLineHighlight( 83 | pre: HTMLPreElement, 84 | params: string[], 85 | initHandlers: HandlerSet, 86 | ) { 87 | const lineHightlightParamIdx = params.findIndex((param) => 88 | braceSurroundingRegex.test(param) 89 | ); 90 | if (lineHightlightParamIdx === -1) return ; 91 | 92 | pre.dataset.line = params[lineHightlightParamIdx].slice(1, -1); 93 | 94 | const initLineHighlight = () => { 95 | window.Prism.plugins.lineHighlight.highlightLines(pre)(); 96 | } 97 | 98 | initHandlers.push(initLineHighlight); 99 | } 100 | 101 | export function commonCodeblockPostProcessor( 102 | element: HTMLElement, 103 | context: MarkdownPostProcessorContext, 104 | app: App, 105 | plugin: Plugin 106 | ) { 107 | // check if processor should run 108 | const processResult = processParams(element, context, app); 109 | if (!processResult) return; 110 | 111 | const { pre, params } = processResult; 112 | const initHandlers: HandlerSet = []; 113 | 114 | // add line numbers. 115 | handleLineNumbers(pre, params, initHandlers); 116 | 117 | // add line highlight 118 | handleLineHighlight(pre, params, initHandlers); 119 | 120 | // Reinit after mount 121 | onMounted(pre, () => { 122 | initHandlers.forEach((handler) => { 123 | handler(); 124 | }); 125 | }); 126 | 127 | // Reinit after resize 128 | plugin.registerEvent(app.workspace.on('resize', () => { 129 | initHandlers.forEach((handler) => { 130 | handler(); 131 | }) 132 | })) 133 | } 134 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* common */ 2 | pre[class*="language-"] { 3 | padding-top: 1em !important; 4 | } 5 | 6 | pre[class*="language-"] > code { 7 | padding: 0 !important; 8 | margin: 0 !important; 9 | } 10 | 11 | /* line-number */ 12 | code[class*="language-"] { 13 | overflow: visible; 14 | } 15 | 16 | pre[class*="language-"].line-numbers { 17 | position: relative; 18 | padding-left: 3.8em; 19 | counter-reset: linenumber; 20 | white-space: pre-wrap; 21 | } 22 | 23 | pre[class*="language-"].line-numbers > code { 24 | position: relative; 25 | white-space: inherit; 26 | } 27 | 28 | .line-numbers .line-numbers-rows { 29 | position: absolute; 30 | pointer-events: none; 31 | top: 0; 32 | font-size: 100%; 33 | left: -3.8em; 34 | width: 3em; /* works for line-numbers below 1000 lines */ 35 | letter-spacing: -1px; 36 | border-right: 1px solid #999; 37 | 38 | -webkit-user-select: none; 39 | -moz-user-select: none; 40 | -ms-user-select: none; 41 | user-select: none; 42 | } 43 | 44 | .line-numbers-rows > span { 45 | display: block; 46 | counter-increment: linenumber; 47 | } 48 | 49 | .line-numbers-rows > span:before { 50 | content: counter(linenumber); 51 | color: #999; 52 | display: block; 53 | padding-right: 0.8em; 54 | text-align: right; 55 | white-space: nowrap; 56 | } 57 | 58 | span.live-preview-codeblock-line-nums { 59 | display: inline-block; 60 | width: 3em; 61 | margin-right: 1.8em; 62 | color: #999; 63 | border-right: 1px solid #999; 64 | padding-right: 0.8em; 65 | text-align: right; 66 | white-space: nowrap; 67 | user-select: none; 68 | } 69 | 70 | /* line-highlight */ 71 | pre[data-line] { 72 | position: relative; 73 | padding: 1em 0 1em 3em; 74 | } 75 | 76 | .line-highlight { 77 | position: absolute; 78 | left: 0; 79 | right: 0; 80 | padding: inherit 0; 81 | margin-top: 1em; /* Same as .prism’s padding-top */ 82 | 83 | background: hsla(24, 20%, 50%, 0.08); 84 | background: linear-gradient( 85 | to right, 86 | hsla(24, 20%, 50%, 0.1) 70%, 87 | hsla(24, 20%, 50%, 0) 88 | ); 89 | 90 | pointer-events: none; 91 | 92 | line-height: inherit; 93 | white-space: pre; 94 | } 95 | 96 | @media print { 97 | .line-highlight { 98 | /* 99 | * This will prevent browsers from replacing the background color with white. 100 | * It's necessary because the element is layered on top of the displayed code. 101 | */ 102 | -webkit-print-color-adjust: exact; 103 | color-adjust: exact; 104 | } 105 | } 106 | 107 | .line-highlight:before, 108 | .line-highlight[data-end]:after { 109 | content: attr(data-start); 110 | position: absolute; 111 | top: 0.4em; 112 | left: 0.6em; 113 | min-width: 1em; 114 | padding: 0 0.5em; 115 | background-color: hsla(24, 20%, 50%, 0.4); 116 | color: hsl(24, 20%, 95%); 117 | font: bold 65%/1.5 sans-serif; 118 | text-align: center; 119 | vertical-align: 0.3em; 120 | border-radius: 999px; 121 | text-shadow: none; 122 | box-shadow: 0 1px white; 123 | } 124 | 125 | .line-highlight[data-end]:after { 126 | content: attr(data-end); 127 | top: auto; 128 | bottom: 0.4em; 129 | } 130 | 131 | .line-numbers .line-highlight:before, 132 | .line-numbers .line-highlight:after { 133 | content: none; 134 | } 135 | 136 | pre[id].linkable-line-numbers span.line-numbers-rows { 137 | pointer-events: all; 138 | } 139 | pre[id].linkable-line-numbers span.line-numbers-rows > span:before { 140 | cursor: pointer; 141 | } 142 | pre[id].linkable-line-numbers span.line-numbers-rows > span:hover:before { 143 | background-color: rgba(128, 128, 128, 0.2); 144 | } 145 | 146 | .HyperMD-codeblock-bg.live-preview-codeblock-highlight { 147 | background: hsla(24, 20%, 50%, 0.08); 148 | background: linear-gradient( 149 | to right, 150 | hsla(24, 20%, 50%, 0.1) 70%, 151 | hsla(24, 20%, 50%, 0) 152 | ); 153 | } 154 | -------------------------------------------------------------------------------- /src/CM6Extensions.ts: -------------------------------------------------------------------------------- 1 | import { ViewPlugin, ViewUpdate, EditorView, DecorationSet, Decoration, WidgetType } from '@codemirror/view' 2 | // @ts-ignore 3 | import { RangeSetBuilder } from '@codemirror/state' 4 | // @ts-ignore 5 | import { syntaxTree, lineClassNodeProp } from '@codemirror/language' 6 | // import { lineClassNodeProp } from '@codemirror/stream-parser' 7 | import { braceSurroundingRegex, paramRegex } from './util' 8 | 9 | interface CodeblockInfo { 10 | showLineNumbers: boolean; 11 | highlightLines: number[] | null; 12 | } 13 | 14 | class LineNumberWidget extends WidgetType { 15 | idx: number; 16 | 17 | constructor(idx: number) { 18 | super(); 19 | this.idx = idx 20 | } 21 | 22 | toDOM() { 23 | const el = document.createElement('span'); 24 | el.className = 'live-preview-codeblock-line-nums'; 25 | el.textContent = '' + this.idx; 26 | return el; 27 | } 28 | } 29 | 30 | export const livePreviewCM6Extension = ViewPlugin.fromClass(class { 31 | decorations: DecorationSet 32 | 33 | constructor(view: EditorView) { 34 | this.decorations = this.buildDecorations(view); 35 | } 36 | 37 | update(update: ViewUpdate) { 38 | if (update.docChanged || update.viewportChanged) this.decorations = this.buildDecorations(update.view); 39 | } 40 | 41 | destory() {} 42 | 43 | buildDecorations(view: EditorView) { 44 | const builder = new RangeSetBuilder(); 45 | const codeblockInfo: CodeblockInfo = { 46 | showLineNumbers: false, 47 | highlightLines: null, 48 | } 49 | let startLineNum: number; 50 | 51 | for (const {from, to} of view.visibleRanges) { 52 | try { 53 | const tree = syntaxTree(view.state) 54 | 55 | tree.iterate({ 56 | from, to, 57 | // @ts-ignore 58 | enter: ({type, from, to}) => { 59 | const lineClasses = type.prop(lineClassNodeProp) 60 | 61 | if (!lineClasses) return ; 62 | const classes = new Set(lineClasses.split(' ')); 63 | const isCodeblockBegin = classes.has('HyperMD-codeblock-begin'); 64 | const isCodeblockLine = 65 | classes.has('HyperMD-codeblock-bg') 66 | && !classes.has('HyperMD-codeblock-begin') 67 | && !classes.has('HyperMD-codeblock-end'); 68 | 69 | // reset data when found codeblock begin line. 70 | if (isCodeblockBegin) { 71 | const startLine = view.state.doc.lineAt(from); 72 | const codeblockParams = startLine.text.match(paramRegex).slice(1); 73 | const highlightParam = codeblockParams.find((param) => braceSurroundingRegex.test(param))?.slice(1, -1); 74 | 75 | startLineNum = startLine.number; 76 | codeblockInfo.showLineNumbers = false; 77 | codeblockInfo.highlightLines = null; 78 | 79 | if (codeblockParams.includes('nums')) codeblockInfo.showLineNumbers = true; 80 | if (highlightParam) codeblockInfo.highlightLines = highlightParam.replace(' ', '').split(',').flatMap((line) => { 81 | if (!+line) { 82 | const res = []; 83 | const [start, end] = line.split('-'); 84 | for (let i = +start; i <= +end; i++) { 85 | res.push(i); 86 | } 87 | 88 | return res; 89 | } 90 | 91 | return [+line]; 92 | }); 93 | } 94 | 95 | if (!isCodeblockLine) return ; 96 | 97 | const currentLineNum = view.state.doc.lineAt(from).number; 98 | 99 | if (codeblockInfo.showLineNumbers) { 100 | const deco = Decoration.widget({ 101 | widget: new LineNumberWidget(currentLineNum - startLineNum), 102 | side: -10000 103 | }); 104 | builder.add(from, from, deco); 105 | } 106 | 107 | if (codeblockInfo.highlightLines) { 108 | if (codeblockInfo.highlightLines.includes(currentLineNum - startLineNum)) { 109 | const line = view.state.doc.lineAt(from); 110 | const deco = Decoration.line({ 111 | attributes: {class: 'live-preview-codeblock-highlight'} 112 | }) 113 | 114 | // @ts-ignore 115 | if (builder.last?.startSide) { 116 | // @ts-ignore 117 | deco.startSide = builder.last.startSide; 118 | deco.endSide = deco.startSide 119 | } 120 | 121 | builder.add(line.from, line.from, deco); 122 | } 123 | } 124 | } 125 | }) 126 | } catch (error) { 127 | console.log(error) 128 | } 129 | } 130 | 131 | return builder.finish(); 132 | } 133 | }, 134 | { 135 | decorations: v => v.decorations 136 | }) -------------------------------------------------------------------------------- /lib/prism-line-numbers.js: -------------------------------------------------------------------------------- 1 | export function extendLineNumberPlugin (Prism) { 2 | 3 | if (typeof Prism === 'undefined' || typeof document === 'undefined') { 4 | return; 5 | } 6 | 7 | /** 8 | * Plugin name which is used as a class name for
 which is activating the plugin
  9 | 	 *
 10 | 	 * @type {string}
 11 | 	 */
 12 | 	var PLUGIN_NAME = 'line-numbers';
 13 | 
 14 | 	/**
 15 | 	 * Regular expression used for determining line breaks
 16 | 	 *
 17 | 	 * @type {RegExp}
 18 | 	 */
 19 | 	var NEW_LINE_EXP = /\n(?!$)/g;
 20 | 
 21 | 
 22 | 	/**
 23 | 	 * Global exports
 24 | 	 */
 25 | 	var config = Prism.plugins.lineNumbers = {
 26 | 		/**
 27 | 		 * Get node for provided line number
 28 | 		 *
 29 | 		 * @param {Element} element pre element
 30 | 		 * @param {number} number line number
 31 | 		 * @returns {Element|undefined}
 32 | 		 */
 33 | 		getLine: function (element, number) {
 34 | 			if (element.tagName !== 'PRE' || !element.classList.contains(PLUGIN_NAME)) {
 35 | 				return;
 36 | 			}
 37 | 
 38 | 			var lineNumberRows = element.querySelector('.line-numbers-rows');
 39 | 			if (!lineNumberRows) {
 40 | 				return;
 41 | 			}
 42 | 			var lineNumberStart = parseInt(element.getAttribute('data-start'), 10) || 1;
 43 | 			var lineNumberEnd = lineNumberStart + (lineNumberRows.children.length - 1);
 44 | 
 45 | 			if (number < lineNumberStart) {
 46 | 				number = lineNumberStart;
 47 | 			}
 48 | 			if (number > lineNumberEnd) {
 49 | 				number = lineNumberEnd;
 50 | 			}
 51 | 
 52 | 			var lineIndex = number - lineNumberStart;
 53 | 
 54 | 			return lineNumberRows.children[lineIndex];
 55 | 		},
 56 | 
 57 | 		/**
 58 | 		 * Resizes the line numbers of the given element.
 59 | 		 *
 60 | 		 * This function will not add line numbers. It will only resize existing ones.
 61 | 		 *
 62 | 		 * @param {HTMLElement} element A `
` element with line numbers.
 63 | 		 * @returns {void}
 64 | 		 */
 65 | 		resize: function (element) {
 66 | 			resizeElements([element]);
 67 | 		},
 68 | 
 69 | 		/**
 70 | 		 * Whether the plugin can assume that the units font sizes and margins are not depended on the size of
 71 | 		 * the current viewport.
 72 | 		 *
 73 | 		 * Setting this to `true` will allow the plugin to do certain optimizations for better performance.
 74 | 		 *
 75 | 		 * Set this to `false` if you use any of the following CSS units: `vh`, `vw`, `vmin`, `vmax`.
 76 | 		 *
 77 | 		 * @type {boolean}
 78 | 		 */
 79 | 		assumeViewportIndependence: true
 80 | 	};
 81 | 
 82 | 	/**
 83 | 	 * Resizes the given elements.
 84 | 	 *
 85 | 	 * @param {HTMLElement[]} elements
 86 | 	 */
 87 | 	function resizeElements(elements) {
 88 | 		elements = elements.filter(function (e) {
 89 | 			var codeStyles = getStyles(e);
 90 | 			var whiteSpace = codeStyles['white-space'];
 91 | 			return whiteSpace === 'pre-wrap' || whiteSpace === 'pre-line';
 92 | 		});
 93 | 
 94 | 		if (elements.length == 0) {
 95 | 			return;
 96 | 		}
 97 | 
 98 | 		var infos = elements.map(function (element) {
 99 | 			var codeElement = element.querySelector('code');
100 | 			var lineNumbersWrapper = element.querySelector('.line-numbers-rows');
101 | 			if (!codeElement || !lineNumbersWrapper) {
102 | 				return undefined;
103 | 			}
104 | 
105 | 			/** @type {HTMLElement} */
106 | 			var lineNumberSizer = element.querySelector('.line-numbers-sizer');
107 | 			var codeLines = codeElement.textContent.split(NEW_LINE_EXP);
108 | 
109 | 			if (!lineNumberSizer) {
110 | 				lineNumberSizer = document.createElement('span');
111 | 				lineNumberSizer.className = 'line-numbers-sizer';
112 | 
113 | 				codeElement.appendChild(lineNumberSizer);
114 | 			}
115 | 
116 | 			lineNumberSizer.innerHTML = '0';
117 | 			lineNumberSizer.style.display = 'block';
118 | 
119 | 			var oneLinerHeight = lineNumberSizer.getBoundingClientRect().height;
120 | 			lineNumberSizer.innerHTML = '';
121 | 
122 | 			return {
123 | 				element: element,
124 | 				lines: codeLines,
125 | 				lineHeights: [],
126 | 				oneLinerHeight: oneLinerHeight,
127 | 				sizer: lineNumberSizer,
128 | 			};
129 | 		}).filter(Boolean);
130 | 
131 | 		infos.forEach(function (info) {
132 | 			var lineNumberSizer = info.sizer;
133 | 			var lines = info.lines;
134 | 			var lineHeights = info.lineHeights;
135 | 			var oneLinerHeight = info.oneLinerHeight;
136 | 
137 | 			lineHeights[lines.length - 1] = undefined;
138 | 			lines.forEach(function (line, index) {
139 | 				if (line && line.length > 1) {
140 | 					var e = lineNumberSizer.appendChild(document.createElement('span'));
141 | 					e.style.display = 'block';
142 | 					e.textContent = line;
143 | 				} else {
144 | 					lineHeights[index] = oneLinerHeight;
145 | 				}
146 | 			});
147 | 		});
148 | 
149 | 		infos.forEach(function (info) {
150 | 			var lineNumberSizer = info.sizer;
151 | 			var lineHeights = info.lineHeights;
152 | 
153 | 			var childIndex = 0;
154 | 			for (var i = 0; i < lineHeights.length; i++) {
155 | 				if (lineHeights[i] === undefined) {
156 | 					lineHeights[i] = lineNumberSizer.children[childIndex++].getBoundingClientRect().height;
157 | 				}
158 | 			}
159 | 		});
160 | 
161 | 		infos.forEach(function (info) {
162 | 			var lineNumberSizer = info.sizer;
163 | 			var wrapper = info.element.querySelector('.line-numbers-rows');
164 | 
165 | 			lineNumberSizer.style.display = 'none';
166 | 			lineNumberSizer.innerHTML = '';
167 | 
168 | 			info.lineHeights.forEach(function (height, lineNumber) {
169 | 				wrapper.children[lineNumber].style.height = height + 'px';
170 | 			});
171 | 		});
172 | 	}
173 | 
174 | 	/**
175 | 	 * Returns style declarations for the element
176 | 	 *
177 | 	 * @param {Element} element
178 | 	 */
179 | 	function getStyles(element) {
180 | 		if (!element) {
181 | 			return null;
182 | 		}
183 | 
184 | 		return window.getComputedStyle ? getComputedStyle(element) : (element.currentStyle || null);
185 | 	}
186 | 
187 | 	// var lastWidth = undefined;
188 | 	// window.addEventListener('resize', function () {
189 | 	// 	if (config.assumeViewportIndependence && lastWidth === window.innerWidth) {
190 | 	// 		return;
191 | 	// 	}
192 | 	// 	lastWidth = window.innerWidth;
193 | 
194 | 	// 	resizeElements(Array.prototype.slice.call(document.querySelectorAll('pre.' + PLUGIN_NAME)));
195 | 	// });
196 | 
197 | 	Prism.hooks.add('complete', function (env) {
198 | 		if (!env.code) {
199 | 			return;
200 | 		}
201 | 
202 | 		var code = /** @type {Element} */ (env.element);
203 | 		var pre = /** @type {HTMLElement} */ (code.parentNode);
204 | 
205 | 		// works only for  wrapped inside 
 (not inline)
206 | 		if (!pre || !/pre/i.test(pre.nodeName)) {
207 | 			return;
208 | 		}
209 | 
210 | 		// Abort if line numbers already exists
211 | 		if (code.querySelector('.line-numbers-rows')) {
212 | 			return;
213 | 		}
214 | 
215 | 		// only add line numbers if  or one of its ancestors has the `line-numbers` class
216 | 		if (!Prism.util.isActive(code, PLUGIN_NAME)) {
217 | 			return;
218 | 		}
219 | 
220 | 		// Remove the class 'line-numbers' from the 
221 | 		code.classList.remove(PLUGIN_NAME);
222 | 		// Add the class 'line-numbers' to the 
223 | 		pre.classList.add(PLUGIN_NAME);
224 | 
225 | 		var match = env.code.match(NEW_LINE_EXP);
226 | 		var linesNum = match ? match.length + 1 : 1;
227 | 		var lineNumbersWrapper;
228 | 
229 | 		var lines = new Array(linesNum + 1).join('');
230 | 
231 | 		lineNumbersWrapper = document.createElement('span');
232 | 		lineNumbersWrapper.setAttribute('aria-hidden', 'true');
233 | 		lineNumbersWrapper.className = 'line-numbers-rows';
234 | 		lineNumbersWrapper.innerHTML = lines;
235 | 
236 | 		if (pre.hasAttribute('data-start')) {
237 | 			pre.style.counterReset = 'linenumber ' + (parseInt(pre.getAttribute('data-start'), 10) - 1);
238 | 		}
239 | 
240 | 		env.element.appendChild(lineNumbersWrapper);
241 | 
242 | 		resizeElements([pre]);
243 | 
244 | 		Prism.hooks.run('line-numbers', env);
245 | 	});
246 | 
247 | 	Prism.hooks.add('line-numbers', function (env) {
248 | 		env.plugins = env.plugins || {};
249 | 		env.plugins.lineNumbers = true;
250 | 	});
251 | 
252 | }
253 | 


--------------------------------------------------------------------------------
/lib/prism-line-highlight.js:
--------------------------------------------------------------------------------
  1 | export function extendLineHighlightPlugin (Prism) {
  2 | 
  3 | 	if (typeof Prism === 'undefined' || typeof document === 'undefined' || !document.querySelector) {
  4 | 		return;
  5 | 	}
  6 | 
  7 | 	var LINE_NUMBERS_CLASS = 'line-numbers';
  8 | 	var LINKABLE_LINE_NUMBERS_CLASS = 'linkable-line-numbers';
  9 | 	var NEW_LINE_REGEX = /\n(?!$)/g
 10 | 
 11 | 	/**
 12 | 	 * @param {string} selector
 13 | 	 * @param {ParentNode} [container]
 14 | 	 * @returns {HTMLElement[]}
 15 | 	 */
 16 | 	function $$(selector, container) {
 17 | 		return Array.prototype.slice.call((container || document).querySelectorAll(selector));
 18 | 	}
 19 | 
 20 | 	/**
 21 | 	 * Returns whether the given element has the given class.
 22 | 	 *
 23 | 	 * @param {Element} element
 24 | 	 * @param {string} className
 25 | 	 * @returns {boolean}
 26 | 	 */
 27 | 	function hasClass(element, className) {
 28 | 		return element.classList.contains(className);
 29 | 	}
 30 | 
 31 | 	/**
 32 | 	 * Calls the given function.
 33 | 	 *
 34 | 	 * @param {() => any} func
 35 | 	 * @returns {void}
 36 | 	 */
 37 | 	function callFunction(func) {
 38 | 		func();
 39 | 	}
 40 | 
 41 | 	// Some browsers round the line-height, others don't.
 42 | 	// We need to test for it to position the elements properly.
 43 | 	var isLineHeightRounded = (function () {
 44 | 		var res;
 45 | 		return function () {
 46 | 			if (typeof res === 'undefined') {
 47 | 				var d = document.createElement('div');
 48 | 				d.style.fontSize = '13px';
 49 | 				d.style.lineHeight = '1.5';
 50 | 				d.style.padding = '0';
 51 | 				d.style.border = '0';
 52 | 				d.innerHTML = ' 
 '; 53 | document.body.appendChild(d); 54 | // Browsers that round the line-height should have offsetHeight === 38 55 | // The others should have 39. 56 | res = d.offsetHeight === 38; 57 | document.body.removeChild(d); 58 | } 59 | return res; 60 | }; 61 | }()); 62 | 63 | /** 64 | * Returns the top offset of the content box of the given parent and the content box of one of its children. 65 | * 66 | * @param {HTMLElement} parent 67 | * @param {HTMLElement} child 68 | */ 69 | function getContentBoxTopOffset(parent, child) { 70 | var parentStyle = getComputedStyle(parent); 71 | var childStyle = getComputedStyle(child); 72 | 73 | /** 74 | * Returns the numeric value of the given pixel value. 75 | * 76 | * @param {string} px 77 | */ 78 | function pxToNumber(px) { 79 | return +px.substr(0, px.length - 2); 80 | } 81 | 82 | return child.offsetTop 83 | + pxToNumber(childStyle.borderTopWidth) 84 | + pxToNumber(childStyle.paddingTop) 85 | - pxToNumber(parentStyle.paddingTop); 86 | } 87 | 88 | /** 89 | * Returns whether the Line Highlight plugin is active for the given element. 90 | * 91 | * If this function returns `false`, do not call `highlightLines` for the given element. 92 | * 93 | * @param {HTMLElement | null | undefined} pre 94 | * @returns {boolean} 95 | */ 96 | function isActiveFor(pre) { 97 | if (!pre || !/pre/i.test(pre.nodeName)) { 98 | return false; 99 | } 100 | 101 | if (pre.hasAttribute('data-line')) { 102 | return true; 103 | } 104 | 105 | if (pre.id && Prism.util.isActive(pre, LINKABLE_LINE_NUMBERS_CLASS)) { 106 | // Technically, the line numbers plugin is also necessary but this plugin doesn't control the classes of 107 | // the line numbers plugin, so we can't assume that they are present. 108 | return true; 109 | } 110 | 111 | return false; 112 | } 113 | 114 | var scrollIntoView = true; 115 | 116 | Prism.plugins.lineHighlight = { 117 | /** 118 | * Highlights the lines of the given pre. 119 | * 120 | * This function is split into a DOM measuring and mutate phase to improve performance. 121 | * The returned function mutates the DOM when called. 122 | * 123 | * @param {HTMLElement} pre 124 | * @param {string | null} [lines] 125 | * @param {string} [classes=''] 126 | * @returns {() => void} 127 | */ 128 | highlightLines: function highlightLines(pre, lines, classes) { 129 | lines = typeof lines === 'string' ? lines : (pre.getAttribute('data-line') || ''); 130 | 131 | var ranges = lines.replace(/\s+/g, '').split(',').filter(Boolean); 132 | var offset = +pre.getAttribute('data-line-offset') || 0; 133 | 134 | var parseMethod = isLineHeightRounded() ? parseInt : parseFloat; 135 | var codeElement = pre.querySelector('code'); 136 | var lineHeight = parseMethod(getComputedStyle(codeElement).lineHeight); 137 | var hasLineNumbers = Prism.util.isActive(pre, LINE_NUMBERS_CLASS); 138 | var parentElement = hasLineNumbers ? pre : codeElement || pre; 139 | var mutateActions = /** @type {(() => void)[]} */ ([]); 140 | var lineBreakMatch = codeElement.textContent.match(NEW_LINE_REGEX); 141 | var numberOfLines = lineBreakMatch? lineBreakMatch.length + 1: 1 142 | 143 | /** 144 | * The top offset between the content box of the element and the content box of the parent element of 145 | * the line highlight element (either `
` or ``).
146 | 			 *
147 | 			 * This offset might not be zero for some themes where the  element has a top margin. Some plugins
148 | 			 * (or users) might also add element above the  element. Because the line highlight is aligned relative
149 | 			 * to the 
 element, we have to take this into account.
150 | 			 *
151 | 			 * This offset will be 0 if the parent element of the line highlight element is the `` element.
152 | 			 */
153 | 			var codePreOffset = !codeElement || parentElement == codeElement ? 0 : getContentBoxTopOffset(pre, codeElement);
154 | 
155 | 			ranges.forEach(function (currentRange) {
156 | 				var range = currentRange.split('-');
157 | 
158 | 				var start = +range[0];
159 | 				var end = +range[1] || start;
160 | 				end = Math.min(numberOfLines, end);
161 | 
162 | 				if (end < start) return ;
163 | 
164 | 				/** @type {HTMLElement} */
165 | 				var line = pre.querySelector('.line-highlight[data-range="' + currentRange + '"]') || document.createElement('div');
166 | 
167 | 				mutateActions.push(function () {
168 | 					line.setAttribute('aria-hidden', 'true');
169 | 					line.setAttribute('data-range', currentRange);
170 | 					line.className = (classes || '') + ' line-highlight';
171 | 				});
172 | 
173 | 				// if the line-numbers plugin is enabled, then there is no reason for this plugin to display the line numbers
174 | 				if (hasLineNumbers && Prism.plugins.lineNumbers) {
175 | 					var startNode = Prism.plugins.lineNumbers.getLine(pre, start);
176 | 					var endNode = Prism.plugins.lineNumbers.getLine(pre, end);
177 | 
178 | 					if (startNode) {
179 | 						var top = startNode.offsetTop + codePreOffset + 'px';
180 | 						mutateActions.push(function () {
181 | 							line.style.top = top;
182 | 						});
183 | 					}
184 | 
185 | 					if (endNode) {
186 | 						var height = (endNode.offsetTop - startNode.offsetTop) + endNode.offsetHeight + 'px';
187 | 						mutateActions.push(function () {
188 | 							line.style.height = height;
189 | 						});
190 | 					}
191 | 					
192 | 				} else {
193 | 					mutateActions.push(function () {
194 | 						line.setAttribute('data-start', String(start));
195 | 
196 | 						if (end > start) {
197 | 							line.setAttribute('data-end', String(end));
198 | 						}
199 | 
200 | 						line.style.top = (start - offset - 1) * lineHeight + codePreOffset + 'px';
201 | 
202 | 						line.textContent = new Array(end - start + 2).join(' \n');
203 | 					});
204 | 				}
205 | 
206 | 				mutateActions.push(function () {
207 | 					line.style.width = pre.scrollWidth + 'px';
208 | 				});
209 | 
210 | 				mutateActions.push(function () {
211 | 					// allow this to play nicely with the line-numbers plugin
212 | 					// need to attack to pre as when line-numbers is enabled, the code tag is relatively which screws up the positioning
213 | 					parentElement.appendChild(line);
214 | 				});
215 | 			});
216 | 
217 | 			var id = pre.id;
218 | 			if (hasLineNumbers && Prism.util.isActive(pre, LINKABLE_LINE_NUMBERS_CLASS) && id) {
219 | 				// This implements linkable line numbers. Linkable line numbers use Line Highlight to create a link to a
220 | 				// specific line. For this to work, the pre element has to:
221 | 				//  1) have line numbers,
222 | 				//  2) have the `linkable-line-numbers` class or an ascendant that has that class, and
223 | 				//  3) have an id.
224 | 
225 | 				if (!hasClass(pre, LINKABLE_LINE_NUMBERS_CLASS)) {
226 | 					// add class to pre
227 | 					mutateActions.push(function () {
228 | 						pre.classList.add(LINKABLE_LINE_NUMBERS_CLASS);
229 | 					});
230 | 				}
231 | 
232 | 				var start = parseInt(pre.getAttribute('data-start') || '1');
233 | 
234 | 				// iterate all line number spans
235 | 				$$('.line-numbers-rows > span', pre).forEach(function (lineSpan, i) {
236 | 					var lineNumber = i + start;
237 | 					lineSpan.onclick = function () {
238 | 						var hash = id + '.' + lineNumber;
239 | 
240 | 						// this will prevent scrolling since the span is obviously in view
241 | 						scrollIntoView = false;
242 | 						location.hash = hash;
243 | 						setTimeout(function () {
244 | 							scrollIntoView = true;
245 | 						}, 1);
246 | 					};
247 | 				});
248 | 			}
249 | 
250 | 			return function () {
251 | 				mutateActions.forEach(callFunction);
252 | 			};
253 | 		}
254 | 	};
255 | 
256 | 
257 | 	function applyHash() {
258 | 		var hash = location.hash.slice(1);
259 | 
260 | 		// Remove pre-existing temporary lines
261 | 		$$('.temporary.line-highlight').forEach(function (line) {
262 | 			line.parentNode.removeChild(line);
263 | 		});
264 | 
265 | 		var range = (hash.match(/\.([\d,-]+)$/) || [, ''])[1];
266 | 
267 | 		if (!range || document.getElementById(hash)) {
268 | 			return;
269 | 		}
270 | 
271 | 		var id = hash.slice(0, hash.lastIndexOf('.'));
272 | 		var pre = document.getElementById(id);
273 | 
274 | 		if (!pre) {
275 | 			return;
276 | 		}
277 | 
278 | 		if (!pre.hasAttribute('data-line')) {
279 | 			pre.setAttribute('data-line', '');
280 | 		}
281 | 
282 | 		var mutateDom = Prism.plugins.lineHighlight.highlightLines(pre, range, 'temporary ');
283 | 		mutateDom();
284 | 
285 | 		if (scrollIntoView) {
286 | 			document.querySelector('.temporary.line-highlight').scrollIntoView();
287 | 		}
288 | 	}
289 | 
290 | 	var fakeTimer = 0; // Hack to limit the number of times applyHash() runs
291 | 
292 | 	Prism.hooks.add('before-sanity-check', function (env) {
293 | 		var pre = env.element.parentElement;
294 | 		if (!isActiveFor(pre)) {
295 | 			return;
296 | 		}
297 | 
298 | 		/*
299 | 		 * Cleanup for other plugins (e.g. autoloader).
300 | 		 *
301 | 		 * Sometimes  blocks are highlighted multiple times. It is necessary
302 | 		 * to cleanup any left-over tags, because the whitespace inside of the 
303 | * tags change the content of the tag. 304 | */ 305 | var num = 0; 306 | $$('.line-highlight', pre).forEach(function (line) { 307 | num += line.textContent.length; 308 | line.parentNode.removeChild(line); 309 | }); 310 | // Remove extra whitespace 311 | if (num && /^(?: \n)+$/.test(env.code.slice(-num))) { 312 | env.code = env.code.slice(0, -num); 313 | } 314 | }); 315 | 316 | Prism.hooks.add('complete', function completeHook(env) { 317 | var pre = env.element.parentElement; 318 | if (!isActiveFor(pre)) { 319 | return; 320 | } 321 | 322 | clearTimeout(fakeTimer); 323 | 324 | var hasLineNumbers = Prism.plugins.lineNumbers; 325 | var isLineNumbersLoaded = env.plugins && env.plugins.lineNumbers; 326 | 327 | if (hasClass(pre, LINE_NUMBERS_CLASS) && hasLineNumbers && !isLineNumbersLoaded) { 328 | Prism.hooks.add('line-numbers', completeHook); 329 | } else { 330 | var mutateDom = Prism.plugins.lineHighlight.highlightLines(pre); 331 | mutateDom(); 332 | fakeTimer = setTimeout(applyHash, 1); 333 | } 334 | }); 335 | 336 | window.addEventListener('hashchange', applyHash); 337 | // window.addEventListener('resize', function () { 338 | // var actions = $$('pre') 339 | // .filter(isActiveFor) 340 | // .map(function (pre) { 341 | // return Prism.plugins.lineHighlight.highlightLines(pre); 342 | // }); 343 | // actions.forEach(callFunction); 344 | // }); 345 | 346 | } 347 | --------------------------------------------------------------------------------