A Wikiful of Hacks: Hacks.Wiki is an experiment to organise quick hacks, notes, bookmarks and tools into an easy-to-build-and-maintain “Digital Garden”.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

1 lines
8.9 KiB

import{EditorSelection,Prec}from"@codemirror/state";import{keymap}from"@codemirror/view";import{defineLanguageFacet,foldNodeProp,indentNodeProp,languageDataProp,Language,LanguageDescription,ParseContext,syntaxTree,LanguageSupport}from"@codemirror/language";import{parser,GFM,Subscript,Superscript,Emoji,MarkdownParser,parseCode}from"@lezer/markdown";import{html}from"@codemirror/lang-html";const data=defineLanguageFacet({block:{open:"\x3c!--",close:"--\x3e"}});const commonmark=parser.configure({props:[foldNodeProp.add(type=>{if(!type.is("Block")||type.is("Document"))return undefined;return(tree,state)=>({from:state.doc.lineAt(tree.from).to,to:tree.to})}),indentNodeProp.add({Document:()=>null}),languageDataProp.add({Document:data})]});function mkLang(parser){return new Language(data,parser,[],"markdown")}const commonmarkLanguage=mkLang(commonmark);const extended=commonmark.configure([GFM,Subscript,Superscript,Emoji]);const markdownLanguage=mkLang(extended);function getCodeParser(languages,defaultLanguage){return info=>{if(info&&languages){let found=null;info=/\S*/.exec(info)[0];if(typeof languages=="function")found=languages(info);else found=LanguageDescription.matchLanguageName(languages,info,true);if(found instanceof LanguageDescription)return found.support?found.support.language.parser:ParseContext.getSkippingParser(found.load());else if(found)return found.parser}return defaultLanguage?defaultLanguage.parser:null}}class Context{constructor(node,from,to,spaceBefore,spaceAfter,type,item){this.node=node;this.from=from;this.to=to;this.spaceBefore=spaceBefore;this.spaceAfter=spaceAfter;this.type=type;this.item=item}blank(maxWidth,trailing=true){let result=this.spaceBefore+(this.node.name=="Blockquote"?">":"");if(maxWidth!=null){while(result.length<maxWidth)result+=" ";return result}else{for(let i=this.to-this.from-result.length-this.spaceAfter.length;i>0;i--)result+=" ";return result+(trailing?this.spaceAfter:"")}}marker(doc,add){let number=this.node.name=="OrderedList"?String(+itemNumber(this.item,doc)[2]+add):"";return this.spaceBefore+number+this.type+this.spaceAfter}}function getContext(node,doc){let nodes=[];for(let cur=node;cur&&cur.name!="Document";cur=cur.parent){if(cur.name=="ListItem"||cur.name=="Blockquote"||cur.name=="FencedCode")nodes.push(cur)}let context=[];for(let i=nodes.length-1;i>=0;i--){let node=nodes[i],match;let line=doc.lineAt(node.from),startPos=node.from-line.from;if(node.name=="FencedCode"){context.push(new Context(node,startPos,startPos,"","","",null))}else if(node.name=="Blockquote"&&(match=/^[ \t]*>( ?)/.exec(line.text.slice(startPos)))){context.push(new Context(node,startPos,startPos+match[0].length,"",match[1],">",null))}else if(node.name=="ListItem"&&node.parent.name=="OrderedList"&&(match=/^([ \t]*)\d+([.)])([ \t]*)/.exec(line.text.slice(startPos)))){let after=match[3],len=match[0].length;if(after.length>=4){after=after.slice(0,after.length-4);len-=4}context.push(new Context(node.parent,startPos,startPos+len,match[1],after,match[2],node))}else if(node.name=="ListItem"&&node.parent.name=="BulletList"&&(match=/^([ \t]*)([-+*])([ \t]{1,4}\[[ xX]\])?([ \t]+)/.exec(line.text.slice(startPos)))){let after=match[4],len=match[0].length;if(after.length>4){after=after.slice(0,after.length-4);len-=4}let type=match[2];if(match[3])type+=match[3].replace(/[xX]/," ");context.push(new Context(node.parent,startPos,startPos+len,match[1],after,type,node))}}return context}function itemNumber(item,doc){return/^(\s*)(\d+)(?=[.)])/.exec(doc.sliceString(item.from,item.from+10))}function renumberList(after,doc,changes,offset=0){for(let prev=-1,node=after;;){if(node.name=="ListItem"){let m=itemNumber(node,doc);let number=+m[2];if(prev>=0){if(number!=prev+1)return;changes.push({from:node.from+m[1].length,to:node.from+m[0].length,insert:String(prev+2+offset)})}prev=number}let next=node.nextSibling;if(!next)break;node=next}}const insertNewlineContinueMarkup=({state,dispatch})=>{let tree=syntaxTree(state),{doc}=state;let dont=null,changes=state.changeByRange(range=>{if(!range.empty||!markdownLanguage.isActiveAt(state,range.from))return dont={range:range};let pos=range.from,line=doc.lineAt(pos);let context=getContext(tree.resolveInner(pos,-1),doc);while(context.length&&context[context.length-1].from>pos-line.from)context.pop();if(!context.length)return dont={range:range};let inner=context[context.length-1];if(inner.to-inner.spaceAfter.length>pos-line.from)return dont={range:range};let emptyLine=pos>=inner.to-inner.spaceAfter.length&&!/\S/.test(line.text.slice(inner.to));if(inner.item&&emptyLine){if(inner.node.firstChild.to>=pos||line.from>0&&!/[^\s>]/.test(doc.lineAt(line.from-1).text)){let next=context.length>1?context[context.length-2]:null;let delTo,insert="";if(next&&next.item){delTo=line.from+next.from;insert=next.marker(doc,1)}else{delTo=line.from+(next?next.to:0)}let changes=[{from:delTo,to:pos,insert:insert}];if(inner.node.name=="OrderedList")renumberList(inner.item,doc,changes,-2);if(next&&next.node.name=="OrderedList")renumberList(next.item,doc,changes);return{range:EditorSelection.cursor(delTo+insert.length),changes:changes}}else{let insert="";for(let i=0,pos=0,e=context.length-2;i<=e;i++){insert+=context[i].blank(i<e?context[i+1].from-pos:null,i<e);pos=context[i].to}insert+=state.lineBreak;return{range:EditorSelection.cursor(pos+insert.length),changes:{from:line.from,insert:insert}}}}if(inner.node.name=="Blockquote"&&emptyLine&&line.from){let prevLine=doc.lineAt(line.from-1),quoted=/>\s*$/.exec(prevLine.text);if(quoted&&quoted.index==inner.from){let changes=state.changes([{from:prevLine.from+quoted.index,to:prevLine.to},{from:line.from+inner.from,to:line.to}]);return{range:range.map(changes),changes:changes}}}let changes=[];if(inner.node.name=="OrderedList")renumberList(inner.item,doc,changes);let insert=state.lineBreak;let continued=inner.item&&inner.item.from<line.from;if(!continued||/^[\s\d.)\-+*>]*/.exec(line.text)[0].length>=inner.to){for(let i=0,pos=0,e=context.length-1;i<=e;i++){insert+=i==e&&!continued?context[i].marker(doc,1):context[i].blank(i<e?context[i+1].from-pos:null);pos=context[i].to}}let from=pos;while(from>line.from&&/\s/.test(line.text.charAt(from-line.from-1)))from--;changes.push({from:from,to:pos,insert:insert});return{range:EditorSelection.cursor(from+insert.length),changes:changes}});if(dont)return false;dispatch(state.update(changes,{scrollIntoView:true,userEvent:"input"}));return true};function isMark(node){return node.name=="QuoteMark"||node.name=="ListMark"}function contextNodeForDelete(tree,pos){let node=tree.resolveInner(pos,-1),scan=pos;if(isMark(node)){scan=node.from;node=node.parent}for(let prev;prev=node.childBefore(scan);){if(isMark(prev)){scan=prev.from}else if(prev.name=="OrderedList"||prev.name=="BulletList"){node=prev.lastChild;scan=node.to}else{break}}return node}const deleteMarkupBackward=({state,dispatch})=>{let tree=syntaxTree(state);let dont=null,changes=state.changeByRange(range=>{let pos=range.from,{doc}=state;if(range.empty&&markdownLanguage.isActiveAt(state,range.from)){let line=doc.lineAt(pos);let context=getContext(contextNodeForDelete(tree,pos),doc);if(context.length){let inner=context[context.length-1];let spaceEnd=inner.to-inner.spaceAfter.length+(inner.spaceAfter?1:0);if(pos-line.from>spaceEnd&&!/\S/.test(line.text.slice(spaceEnd,pos-line.from)))return{range:EditorSelection.cursor(line.from+spaceEnd),changes:{from:line.from+spaceEnd,to:pos}};if(pos-line.from==spaceEnd){let start=line.from+inner.from;if(inner.item&&inner.node.from<inner.item.from&&/\S/.test(line.text.slice(inner.from,inner.to)))return{range:range,changes:{from:start,to:line.from+inner.to,insert:inner.blank(inner.to-inner.from)}};if(start<pos)return{range:EditorSelection.cursor(start),changes:{from:start,to:pos}}}}}return dont={range:range}});if(dont)return false;dispatch(state.update(changes,{scrollIntoView:true,userEvent:"delete"}));return true};const markdownKeymap=[{key:"Enter",run:insertNewlineContinueMarkup},{key:"Backspace",run:deleteMarkupBackward}];const htmlNoMatch=html({matchClosingTags:false});function markdown(config={}){let{codeLanguages,defaultCodeLanguage,addKeymap=true,base:{parser}=commonmarkLanguage}=config;if(!(parser instanceof MarkdownParser))throw new RangeError("Base parser provided to `markdown` should be a Markdown parser");let extensions=config.extensions?[config.extensions]:[];let support=[htmlNoMatch.support],defaultCode;if(defaultCodeLanguage instanceof LanguageSupport){support.push(defaultCodeLanguage.support);defaultCode=defaultCodeLanguage.language}else if(defaultCodeLanguage){defaultCode=defaultCodeLanguage}let codeParser=codeLanguages||defaultCode?getCodeParser(codeLanguages,defaultCode):undefined;extensions.push(parseCode({codeParser:codeParser,htmlParser:htmlNoMatch.language.parser}));if(addKeymap)support.push(Prec.high(keymap.of(markdownKeymap)));return new LanguageSupport(mkLang(parser.configure(extensions)),support)}export{commonmarkLanguage,deleteMarkupBackward,insertNewlineContinueMarkup,markdown,markdownKeymap,markdownLanguage};