html小工具——文章注释编辑器

时间:2023-03-09 15:41:08
html小工具——文章注释编辑器

在网上阅读文章时,读者时常会想针对某段文字写一些自己的感想,可惜大部分阅读网站并不提供这样的功能,读者往往只能将文本复制到本地或在线的编辑器中编辑注释,之后如果想在其他地方回顾这些注释也必须先本地安装或联网登录(云笔记)编辑器。如此操作,麻烦倒在其次,错过灵感才是最让人惋惜的,于是决定编写一个简单的小工具尝试解决这一问题,

一、用法:

用Chrome浏览器打开此工具

html小工具——文章注释编辑器

点击“选择文本文件”选择txt文件,或者直接将纯文本粘贴到文本框中,点击确定则以绿色背景显示出文本内容,鼠标双击绿色正文段落即在下面弹出黄色注释段落,双击注释段落即可编辑注释内容(正文不可编辑),编辑时点击其他地方即可取消编辑,再次双击绿色正文段落则隐藏本段注释。

点击导出html即可将正文和注释内容保存为一个html文档,用Chrome浏览器打开此html文档即可以查看已经录入的正文和注释,另外这个导出的html文档也具有本工具的所有特性,使用者可以继续编辑注释内容,或者完全替换性的加载新的文本,编辑完毕后又可以导出为新的html文档。

二、编写方法:

1、首先拼一个html结构出来:

 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>实用版,处理事件问题</title>
<style>
/*通用属性*/
body{ margin: 0; padding: 0; border: 0; text-align: center; overflow: hidden;width: 100%;
height: 100%;position: fixed; font-family: verdana,arial,sans-serif; touch-action: none;
-ms-touch-action: none;font-size: 12px;min-width: 600px;}
ul { list-style: none; margin: 0; padding: 0;}
li{ list-style: none; margin: 0; padding: 0;}
ul li { float: left;}
button{ cursor: pointer; height: 23px;}
a:link{ text-decoration: none;} #div_top span,input,button,textarea{margin-left: 20px;float: left;}
.p_h1{font-size: 24px;font-weight: bolder;background-color: darkseagreen}
.p_text{font-size: 20px;font-weight: normal;text-align: left;background-color: darkseagreen} .div_section{position:relative}
.p_a1{font-size: 20px;font-weight: normal;text-align: left;background-color: beige;margin-left: 60px;margin-right: 60px;
;min-height: 80px;display: block;word-break: break-all;overflow-wrap: break-word;white-space: pre-wrap}
.p_a2{font-size: 20px;font-weight: normal;text-align: left;background-color: beige;margin-left:120px}
</style>
</head>
<body>
<div id="div_allbase" style="top:0px;bottom:0px;width:100%;height: 100%;position:relative;overflow-x: hidden;overflow-y: scroll
">
<div id="div_top" style="top:0px;left:0px;width:100%;height: 30px;position:absolute;">
<span style="margin-left: 20px">选择文本文件</span>
<input type="file" id="str_local" onchange="ShowStr()" style="margin-left: 20px">
<span style="margin-left: 20px">粘贴文本</span>
<textarea type="text" id="str_local2" style="word-break: break-all;overflow-wrap: break-word;white-space: pre-wrap;height:40px"></textarea>
<button onclick="ShowStr2()">确定</button>
<button onclick="ExportHtml()">导出Html</button>
</div>
<div id="div_main" style="top:30px;left:0px;width:100%;position:absolute;"> </div>
</div>
</body>

其中div_top是上面的操作区,div_main用来存放正文段落和注释段落。这里文本框使用textarea标签而非input标签,因为input标签会自动将多行文本的换行符替换为空格,white-space: pre-wrap样式则负责告知textarea标签保留换行符,否则换行符也会被替换掉。

2、将纯文本转换为正文段落:

 function ShowStr()//从文件读取文本
{
var str_local=document.getElementById("str_local");
var file=str_local.files[0];
var reader=new FileReader();
reader.readAsText(file);
reader.onload=function(e)
{
var str=e.target.result;
loadArticle(str);
}
}
function ShowStr2()//从文本框读取文本
{
var str_local=document.getElementById("str_local2");
var str=str_local.value;
loadArticle(str);
}
var currentSectionid=null;
function loadArticle(str)
{
var div_main=document.getElementById("div_main");
div_main.innerHTML="";
var arr_section=str.split("\r\n");
var len=arr_section.length;
if(len==1)//如果按\r\n分段后只有一段
{
arr_section=str.split("\n");
}
len=arr_section.length;
var count_p=0;//包含主标题在内一共分成几段
var arr_p=[];
for(var i=0;i<len;i++)
{
var section=arr_section[i];
if(section.length>0)
{
let div_section=document.createElement("div");
div_section.className="div_section";//这样可以更方便的在段内插入元素
div_section.id="div_section_"+count_p; let p=document.createElement("p");
if(count_p==0)//标题段
{
p.className="p_h1";
}
else
{
p.className="p_text";
}
p.innerHTML=" "+section;
p.id="p_section_"+count_p; p.ondblclick=function()
{
addAnnotate(div_section.id);
}
count_p++;
div_section.appendChild(p);
div_main.appendChild(div_section);
}
}
}

代码 并不复杂,根据\r\n对文本进行分段(从文本框读取时需要用\n分段),每一段生成对应的html文档插入dom中,每个“section”都有唯一的dom id。目前还没有调试好的是如何在导出导入html时保持每一段前面的四个空格,浏览器会自动将它们去掉。

注意在为批量建立的标签设定事件响应时,需要用let型变量而非var型变量,否则同一循环中定义的所有事件响应都将以循环操作的最后一个标签为目标。

3、双击正文段落时展开注释段落:

 function addAnnotate(id)
{
var div_section=document.getElementById(id);
currentSectionid=id;//这个好像没用到
var children=div_section.childNodes;
var len=children.length;
if(len==1)//此时还没有第一级注释
{
let p_a1=document.createElement("p");
p_a1.className="p_a1";
//点击第一级注释,进行编辑
p_a1.ondblclick=function(){openEdit(p_a1)};
p_a1.onblur=function(){closeEdit(p_a1)};//失去焦点时关闭注释段的编辑状态
div_section.appendChild(p_a1)
}
else
{
if(children[1].style.display=="none")//如果当前是隐藏状态
{
for(var i=1;i<len;i++)
{
children[i].style.display="block";//显示注释段
}
}
else
{
for(var i=1;i<len;i++)
{
var child=children[i];
child.style.display="none";//隐藏注释段
if(child.className=="p_a1")
{
closeEdit(child);
}
}
}
}
}

这里根据sectionid找到当前操作的正文段落div,将空白的注释段落插进去。

4、编辑注释及关闭编辑状态:

 function openEdit(p)
{
p.style.border="2px solid cornflowerblue";
p.style.borderRadius="5px";
p.contentEditable="true";
}
function closeEdit(p)
{
p.style.border="0px";
p.contentEditable="false";
//Chrome的默认编辑模式会在p内插入一个div,这个div是用来分行的,空白的换行也会导致div!!
// 但在重新导入之后(在取innerHTML导出时尚正常)浏览器会自动把这个div绘制在p的外面!!!!
//多次换行是嵌套的!!!!所以简单的替换还不行!!
p.innerHTML=p.innerHTML.replace(new RegExp(("<[^>]+>"),"gm"),"@");//如果有连续的多个xml标签
p.innerHTML=p.innerHTML.replace(new RegExp(("[@]+"),"gm"),"\r\n");
//p.innerHTML=p.innerHTML.replace(new RegExp(("<[^>]+>"),"gm"),"\r\n");
//p.innerHTML=p.innerHTML.replace(new RegExp(("<[^/][\\S]{1,5}>"),"gm"),"\r\n");
//p.innerHTML=p.innerHTML.replace(new RegExp(("</[\\S]{1,6}>"),"gm"),"");
}

这里使用了浏览器内置的“contentEditable”属性编辑p标签内容,这样做的一个缺点是浏览器会在用户编辑标签时不受用户控制的插入各种控制格式的标签,而同样的html再次导入浏览器时,又会按照不同的规则自动排列成别的顺序。(类似浏览器的CSS调试,只预期在运行时一次性使用?)

为解决这一问题,我在关闭p标签的contentEditable状态时,用正则表达式清除了所有的xml标签,但这也将导致新的问题——使用者希望写在注释中的xml标签(包括替换符号@)也会被清除掉,不知道有没有更好的方法解决这一问题。

5、导出html文档:

 function ExportHtml()
{
var str=str_head+window.document.body.outerHTML+"</html>";
var blob=new Blob([str],{//字符串转为二进制流
type: "text/plain"
}) var tmpa = document.createElement("a");
var p_h1=document.getElementsByClassName("p_h1")[0];
tmpa.download = (p_h1?p_h1.innerHTML:"test")+".html";//自动用标题作为文件名
tmpa.href = URL.createObjectURL(blob);//用临时的a标签下载
tmpa.click();//导出后事件需要重新绑定,或者直接使用innHTML定义?
setTimeout(function () {
URL.revokeObjectURL(blob);//释放掉流
}, 100);
}

其中str_head包含了html头部内容,加上目前正在编辑的body内容和html结束标签组成导出的html文档,然后转换为二进制文件流用临时a标签下载。

注意,使用.onxx方式绑定的事件并不会随着html文档一起导出,可以选择在打开html时重新绑定事件,或者在建立标签时使用innerHTML而非appendChild方法,直接将事件响应写在innerHTML里

6、重新打开导出的html文档时重新绑定事件:

 window.onload=function(){
//处理导出html的事件有两种思路,一是使用innerHTML定义所有动态生成的标签,二是在每次打开网页时重新绑定事件
var div_main=document.getElementById("div_main");
var arr_section=div_main.getElementsByClassName("div_section");
var len=arr_section.length;
for(var i=0;i<len;i++)
{
let div_section=arr_section[i];
var arr_p=div_section.getElementsByTagName("p");
var len2=arr_p.length;
for(var j=0;j<len2;j++)
{
let p=arr_p[j];
if(j==0)
{
p.ondblclick=function()
{
addAnnotate(div_section.id);
}
}
else
{
p.ondblclick=function(){openEdit(p)};
p.onblur=function(){closeEdit(p)};
}
}
}
}

如此就完成了文章注释编辑器小工具的编写,下面是完整的代码,复制粘贴到一个空白的.html文件中即可使用。

 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>实用版,处理事件问题</title>
<style>
/*通用属性*/
body{ margin: 0; padding: 0; border: 0; text-align: center; overflow: hidden;width: 100%;
height: 100%;position: fixed; font-family: verdana,arial,sans-serif; touch-action: none;
-ms-touch-action: none;font-size: 12px;min-width: 600px;}
ul { list-style: none; margin: 0; padding: 0;}
li{ list-style: none; margin: 0; padding: 0;}
ul li { float: left;}
button{ cursor: pointer; height: 23px;}
a:link{ text-decoration: none;} #div_top span,input,button,textarea{margin-left: 20px;float: left;}
.p_h1{font-size: 24px;font-weight: bolder;background-color: darkseagreen}
.p_text{font-size: 20px;font-weight: normal;text-align: left;background-color: darkseagreen} .div_section{position:relative}
.p_a1{font-size: 20px;font-weight: normal;text-align: left;background-color: beige;margin-left: 60px;margin-right: 60px;
;min-height: 80px;display: block;word-break: break-all;overflow-wrap: break-word;white-space: pre-wrap}
.p_a2{font-size: 20px;font-weight: normal;text-align: left;background-color: beige;margin-left:120px}
</style>
</head>
<body>
<div id="div_allbase" style="top:0px;bottom:0px;width:100%;height: 100%;position:relative;overflow-x: hidden;overflow-y: scroll
">
<div id="div_top" style="top:0px;left:0px;width:100%;height: 30px;position:absolute;">
<span style="margin-left: 20px">选择文本文件</span>
<input type="file" id="str_local" onchange="ShowStr()" style="margin-left: 20px">
<span style="margin-left: 20px">粘贴文本</span>
<textarea type="text" id="str_local2" style="word-break: break-all;overflow-wrap: break-word;white-space: pre-wrap;height:40px"></textarea>
<button onclick="ShowStr2()">确定</button>
<button onclick="ExportHtml()">导出Html</button>
</div>
<div id="div_main" style="top:30px;left:0px;width:100%;position:absolute;"> </div>
</div>
</body>
<script> window.onload=function(){
//处理导出html的事件有两种思路,一是使用innerHTML定义所有动态生成的标签,二是在每次打开网页时重新绑定事件
var div_main=document.getElementById("div_main");
var arr_section=div_main.getElementsByClassName("div_section");
var len=arr_section.length;
for(var i=0;i<len;i++)
{
let div_section=arr_section[i];
var arr_p=div_section.getElementsByTagName("p");
var len2=arr_p.length;
for(var j=0;j<len2;j++)
{
let p=arr_p[j];
if(j==0)
{
p.ondblclick=function()
{
addAnnotate(div_section.id);
}
}
else
{
p.ondblclick=function(){openEdit(p)};
p.onblur=function(){closeEdit(p)};
}
}
}
}
function ShowStr()
{
var str_local=document.getElementById("str_local");
var file=str_local.files[0];
var reader=new FileReader();
reader.readAsText(file);
reader.onload=function(e)
{
var str=e.target.result;
loadArticle(str);
}
}
function ShowStr2()
{
var str_local=document.getElementById("str_local2");
var str=str_local.value;
loadArticle(str);
}
var currentSectionid=null;
function loadArticle(str)
{
var div_main=document.getElementById("div_main");
div_main.innerHTML="";
var arr_section=str.split("\r\n");
var len=arr_section.length;
if(len==1)
{
arr_section=str.split("\n");
}
len=arr_section.length;
var count_p=0;//包含主标题在内一共分成几段
var arr_p=[];
for(var i=0;i<len;i++)
{
var section=arr_section[i];
if(section.length>0)
{
let div_section=document.createElement("div");
div_section.className="div_section";//这样可以更方便的在段内插入元素
div_section.id="div_section_"+count_p; let p=document.createElement("p");
if(count_p==0)//标题段
{
p.className="p_h1";
}
else
{
p.className="p_text";
}
p.innerHTML=" "+section;
p.id="p_section_"+count_p; p.ondblclick=function()
{
addAnnotate(div_section.id);
}
count_p++;
div_section.appendChild(p);
div_main.appendChild(div_section);
}
}
}
function addAnnotate(id)
{
var div_section=document.getElementById(id);
currentSectionid=id;
var children=div_section.childNodes;
var len=children.length;
if(len==1)//此时还没有第一级注释
{
let p_a1=document.createElement("p");
p_a1.className="p_a1";
//点击第一级注释,进行编辑
p_a1.ondblclick=function(){openEdit(p_a1)};
p_a1.onblur=function(){closeEdit(p_a1)};
div_section.appendChild(p_a1)
}
else
{
if(children[1].style.display=="none")//如果当前是隐藏状态
{
for(var i=1;i<len;i++)
{
children[i].style.display="block";
}
}
else
{
for(var i=1;i<len;i++)
{
var child=children[i];
child.style.display="none";
if(child.className=="p_a1")
{
closeEdit(child);
}
}
}
}
}
function openEdit(p)
{
p.style.border="2px solid cornflowerblue";
p.style.borderRadius="5px";
p.contentEditable="true";
}
function closeEdit(p)
{
p.style.border="0px";
p.contentEditable="false";
//Chrome的默认编辑模式会在p内插入一个div,这个div是用来分行的,空白的换行也会导致div!!
// 但在重新导入之后(在取innerHTML导出时尚正常)浏览器会自动把这个div绘制在p的外面!!!!
//多次换行是嵌套的!!!!所以简单的替换还不行!!
p.innerHTML=p.innerHTML.replace(new RegExp(("<[^>]+>"),"gm"),"@");
p.innerHTML=p.innerHTML.replace(new RegExp(("[@]+"),"gm"),"\r\n");
//p.innerHTML=p.innerHTML.replace(new RegExp(("<[^>]+>"),"gm"),"\r\n");
//p.innerHTML=p.innerHTML.replace(new RegExp(("<[^/][\\S]{1,5}>"),"gm"),"\r\n");
//p.innerHTML=p.innerHTML.replace(new RegExp(("</[\\S]{1,6}>"),"gm"),"");
} function ExportHtml()
{
var str=str_head+window.document.body.outerHTML+"</html>";
var blob=new Blob([str],{
type: "text/plain"
}) var tmpa = document.createElement("a");
var p_h1=document.getElementsByClassName("p_h1")[0];
tmpa.download = (p_h1?p_h1.innerHTML:"test")+".html";
tmpa.href = URL.createObjectURL(blob);
tmpa.click();//导出后事件需要重新绑定,或者直接使用innHTML定义?
setTimeout(function () {
URL.revokeObjectURL(blob);
}, 100);
} var str_head="<!DOCTYPE html>\n" +
"<html lang=\"en\">\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <title>实用版</title>\n" +
" <style>\n" +
" /*通用属性*/\n" +
" body{ margin: 0; padding: 0; border: 0; text-align: center; overflow: hidden;width: 100%;\n" +
" height: 100%;position: fixed; font-family: verdana,arial,sans-serif; touch-action: none;\n" +
" -ms-touch-action: none;font-size: 12px;min-width: 600px;}\n" +
" ul { list-style: none; margin: 0; padding: 0;}\n" +
" li{ list-style: none; margin: 0; padding: 0;}\n" +
" ul li { float: left;}\n" +
" button{ cursor: pointer; height: 23px;}\n" +
" a:link{ text-decoration: none;}\n" +
" \n" +
" #div_top span,input,button,textarea{margin-left: 20px;float: left;}\n" +
" .p_h1{font-size: 24px;font-weight: bolder;background-color: darkseagreen}\n" +
" .p_text{font-size: 20px;font-weight: normal;text-align: left;background-color: darkseagreen}\n" +
"\n" +
" .div_section{position:relative}\n" +
" .p_a1{font-size: 20px;font-weight: normal;text-align: left;background-color: beige;margin-left: 60px;margin-right: 60px;\n" +
" ;min-height: 80px;display: block;word-break: break-all;overflow-wrap: break-word;white-space: pre-wrap}\n" +
" .p_a2{font-size: 20px;font-weight: normal;text-align: left;background-color: beige;left:120px}\n" +
" </style>\n" +
"</head>";
</script>
</html>

这个小工具代码不多,功能也不复杂,但我认为比较独特的一点是它像单细胞生物一样在一个文件中实现了信息的“保持”、“显示”、“修改”功能,应该具备一定的可发展性,也欢迎大家在此基础上开源更多功能。

用作例子的文章是康德的《纯粹理性批判》(民国蓝公武译本)(http://book.sbkk8.com/waiguo/chuncuilixingpipan/

感觉这本书被崇拜者们过誉了,现在更适合作为历史和参考。

2020年2月16日修改:

允许用户添加额外的分隔符,处理额外分隔符的代码如下:

 var str_split=document.getElementById("str_split").value;
if(str_split)//如果有额外的分割符号
{
var arr_temp=[];
var arr_split=str_split.split("@");
var len2=arr_split.length;
var str_temp="/";
for(var j=0;j<len2;j++)
{
str_temp+=(arr_split[j]+(j==len2-1?"":"|"));
}
str_temp+="/";
var var_split=eval(str_temp);
for(var i=0;i<len;i++)//对于每一个自然段
{
//
var section=arr_section[i];
if(section.length>0)
{
arr_temp=arr_temp.concat(section.split(var_split));
//arr_temp=arr_temp.splice(arr_temp.length,0,section.split(var_split))
}
}
arr_section=arr_temp;
len=arr_section.length;
}

新的html文件可以在https://github.com/ljzc002/ljzc002.github.io/blob/master/%E5%93%B2%E5%AD%A6%E5%AD%A6%E4%B9%A0/Annotate4split.html下载。

2020年4月26日修改:

调整页面样式,添加文本导出方法和适用于知乎的样式调整脚本:

 window.ExportZhihu=function()//在控制台里输出所有文本
{
var div_main=document.getElementById("div_main");
var arr_p=div_main.getElementsByTagName("p");
var len=arr_p.length;
var arr_str=[];
for(var i=0;i<len;i++)
{
var p=arr_p[i];
if(p.className=="p_text")
{
arr_str.push(p.innerHTML);
}
else if(p.className=="p_a1")
{
arr_str.push("@"+p.innerHTML+"@");//在注释内容的前后添加@符号
}
}
var str_res=arr_str.join("\r\n");
console.log(str_res)
}

粘贴到知乎发布之后,在知乎页面的控制台里执行

 //RichText ztext Post-RichText
//RichText ztext CopyrightRichText-richText
function addBlockQuoteZhihu(){
var arr_richText=document.getElementsByClassName("RichText ztext");
var len=arr_richText.length;
for(var i=0;i<len;i++)
{
var richText=arr_richText[i];
var arr_c=richText.childNodes;
var len2=arr_c.length;
var temp_HTML="";
//var temp_div=document.createElement("div"); for(var j=0;j<len2;j++)//对于每一个节点元素
{
var flag=0;//还未找到开头#
var node=arr_c[j];
if(node.innerHTML.substr(0,1)=="@"&&node.innerHTML.substr(-1,1)=="@")
{
flag=1;//找到了开头和结尾#
node.innerHTML=node.innerHTML.substring(1);//去掉井号
node.innerHTML=node.innerHTML.substring(node.innerHTML.length-1,0);
temp_HTML+="<blockquote>"+node.outerHTML+"</blockquote>";
//temp_div.appendChild(node.cloneNode());//cloneNode只包含标签,没有innerHTML!!还需要自己填
}
else if(node.innerHTML.substr(0,1)=="@")
{
flag=1;//找到了开头#
node.innerHTML=node.innerHTML.substring(1);
temp_HTML+="<blockquote>"+node.outerHTML;
//temp_div.appendChild(node.cloneNode());
}
else if(node.innerHTML.substr(-1,1)=="@")
{
flag=1;//找到了结尾#
node.innerHTML=node.innerHTML.substring(node.innerHTML.length-1,0);
temp_HTML+=node.outerHTML+"</blockquote>";
//temp_div.appendChild(node.cloneNode());
}
if(flag==0)
{
temp_HTML+=node.outerHTML;
//temp_div.appendChild(node.cloneNode());
}
}
richText.innerHTML=temp_HTML;
//richText.innerHTML=temp_div.innerHTML;
} }//下面时压缩为单行的脚本
//function addBlockQuoteZhihu(){var arr_richText=document.getElementsByClassName("RichText ztext");var len=arr_richText.length;for(var i=0;i<len;i++){var richText=arr_richText[i];var arr_c=richText.childNodes;var len2=arr_c.length;var temp_HTML="";for(var j=0;j<len2;j++){var flag=0;var node=arr_c[j];if(node.innerHTML.substr(0,1)=="@"&&node.innerHTML.substr(-1,1)=="@"){flag=1;node.innerHTML=node.innerHTML.substring(1);node.innerHTML=node.innerHTML.substring(node.innerHTML.length-1,0);temp_HTML+="<blockquote>"+node.outerHTML+"</blockquote>";}else if(node.innerHTML.substr(0,1)=="@"){flag=1;node.innerHTML=node.innerHTML.substring(1);temp_HTML+="<blockquote>"+node.outerHTML;}else if(node.innerHTML.substr(-1,1)=="@"){flag=1;node.innerHTML=node.innerHTML.substring(node.innerHTML.length-1,0);temp_HTML+=node.outerHTML+"</blockquote>";}if(flag==0){temp_HTML+=node.outerHTML;}}richText.innerHTML=temp_HTML;}};addBlockQuoteZhihu()

可以将@之间的文本变为区别于正文的样式,如此在发布长篇文章时可以省去样式调整操作。

下载地址:https://github.com/ljzc002/ljzc002.github.io/blob/master/%E5%93%B2%E5%AD%A6%E5%AD%A6%E4%B9%A0/Annotate5zhihu.html