Adds rich text support to notes
- Still a few issues - Converts plaintext notes to HTML on upgrade
This commit is contained in:
parent
b46860f6a4
commit
651bcf2380
15 changed files with 282 additions and 46 deletions
|
@ -192,7 +192,7 @@
|
|||
</hbox>
|
||||
</vbox>
|
||||
|
||||
<textbox id="editor" type="styled" hidden="true" flex="1"/>
|
||||
<textbox id="editor" type="styled" mode="integration" hidden="true" flex="1"/>
|
||||
|
||||
<hbox style="margin-top: 15px">
|
||||
<hbox>
|
||||
|
|
|
@ -155,11 +155,11 @@
|
|||
textbox.setAttribute('readonly', 'true');
|
||||
}
|
||||
|
||||
var scrollPos = textbox.inputField.scrollTop;
|
||||
//var scrollPos = textbox.inputField.scrollTop;
|
||||
if (this.item) {
|
||||
textbox.value = this.item.getNote();
|
||||
}
|
||||
textbox.inputField.scrollTop = scrollPos;
|
||||
//textbox.inputField.scrollTop = scrollPos;
|
||||
|
||||
this._id('linksbox').hidden = !(this.displayTags && this.displayRelated);
|
||||
|
||||
|
@ -322,7 +322,7 @@
|
|||
<method name="disableUndo">
|
||||
<body>
|
||||
<![CDATA[
|
||||
this.noteField.editor.enableUndo(true);
|
||||
//this.noteField.editor.enableUndo(true);
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
@ -330,7 +330,7 @@
|
|||
<method name="enableUndo">
|
||||
<body>
|
||||
<![CDATA[
|
||||
this.noteField.editor.enableUndo(false);
|
||||
//this.noteField.editor.enableUndo(false);
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
@ -357,7 +357,7 @@
|
|||
<content>
|
||||
<xul:vbox xbl:inherits="flex">
|
||||
<xul:label id="citeLabel"/>
|
||||
<xul:textbox id="noteField" multiline="true" type="timed" timeout="1000" flex="1"/>
|
||||
<xul:textbox id="noteField" type="styled" mode="note" timeout="1000" flex="1"/>
|
||||
<xul:hbox id="linksbox" hidden="true">
|
||||
<xul:linksbox id="links" flex="1"/>
|
||||
</xul:hbox>
|
||||
|
|
|
@ -28,23 +28,33 @@
|
|||
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
||||
<binding id="styled-textbox">
|
||||
<implementation>
|
||||
<field name="_mode"/>
|
||||
<field name="_format"/>
|
||||
<field name="_loadHandler"/>
|
||||
<field name="_commandString"/>
|
||||
<field name="_eventHandler"/>
|
||||
<field name="_timer"/>
|
||||
|
||||
<constructor><![CDATA[
|
||||
this._browser = document.getAnonymousElementByAttribute(this, "anonid", "rt-view");
|
||||
this.mode = this.getAttribute('mode');
|
||||
|
||||
this._iframe = document.getAnonymousElementByAttribute(this, "anonid", "rt-view");
|
||||
this._editor = null;
|
||||
this._value = null;
|
||||
|
||||
this._rtfMap = {
|
||||
"\\":"\\\\",
|
||||
"&":"&",
|
||||
"<":"<",
|
||||
">":">",
|
||||
"<em>":"\\i ",
|
||||
"</em>":"\\i0 ",
|
||||
"<i>":"\\i ",
|
||||
"</i>":"\\i0 ",
|
||||
"<strong>":"\\b ",
|
||||
"</strong>":"\\b0 ",
|
||||
"<b>":"\\b ",
|
||||
"</b>":"\\b0 ",
|
||||
"<u>":"\\ul ",
|
||||
"</u>":"\\ul0 ",
|
||||
"<br>":"\x0B",
|
||||
"<br />":"\x0B",
|
||||
"<sup>":"\\super ",
|
||||
"</sup>":"\\super0 ",
|
||||
"<sub>":"\\sub ",
|
||||
|
@ -56,41 +66,108 @@
|
|||
|
||||
// not sure why an event is necessary here, but it is
|
||||
var me = this;
|
||||
this._loadHandler = function() {me._browserLoaded()};
|
||||
this._browser.addEventListener("DOMContentLoaded", this._loadHandler, false);
|
||||
this._loadHandler = function() {me._iframeLoaded()};
|
||||
this._iframe.addEventListener("DOMContentLoaded", this._loadHandler, false);
|
||||
]]></constructor>
|
||||
|
||||
<!-- Called when browser is loaded. Until the browser is loaded, we can't do
|
||||
<!-- Called when iframe browser is loaded. Until the browser is loaded, we can't do
|
||||
anything with it, so we just keep track of what's supposed to
|
||||
happen. -->
|
||||
<method name="_browserLoaded">
|
||||
<method name="_iframeLoaded">
|
||||
<body><![CDATA[
|
||||
this._browser.removeEventListener("DOMContentLoaded", this._loadHandler, false);
|
||||
this._iframe.removeEventListener("DOMContentLoaded", this._loadHandler, false);
|
||||
|
||||
var ios = Components.classes["@mozilla.org/network/io-service;1"].
|
||||
getService(Components.interfaces.nsIIOService);
|
||||
var uri = ios.newURI("chrome://zotero/content/tiny_mce/integration.html", null, null);
|
||||
var uri = ios.newURI("chrome://zotero/content/tiny_mce/" + this.mode + ".html", null, null);
|
||||
var chromeReg = Components.classes["@mozilla.org/chrome/chrome-registry;1"].
|
||||
getService(Components.interfaces.nsIChromeRegistry);
|
||||
var fileURI = chromeReg.convertChromeURL(uri);
|
||||
|
||||
this._browser.webNavigation.loadURI(fileURI.spec,
|
||||
this._iframe.webNavigation.loadURI(fileURI.spec,
|
||||
Components.interfaces.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, null, null, null);
|
||||
|
||||
// Register handler for deferred setting of content
|
||||
var me = this;
|
||||
|
||||
var listener = function() {
|
||||
me._browser.removeEventListener("DOMContentLoaded", listener, false);
|
||||
var editor = me._browser.contentWindow.wrappedJSObject.tinyMCE.get("tinymce");
|
||||
me._iframe.removeEventListener("DOMContentLoaded", listener, false);
|
||||
var editor = me._iframe.contentWindow.wrappedJSObject.tinyMCE.get("tinymce");
|
||||
|
||||
editor.onInit.add(function() {
|
||||
me._editor = editor;
|
||||
if(me._value) me.value = me._value;
|
||||
});
|
||||
|
||||
if (me._eventHandler) {
|
||||
me._iframe.contentWindow.wrappedJSObject.handleEvent = me._eventHandler;
|
||||
}
|
||||
};
|
||||
this._browser.addEventListener("DOMContentLoaded", listener, false);
|
||||
this._iframe.addEventListener("DOMContentLoaded", listener, false);
|
||||
]]></body>
|
||||
</method>
|
||||
|
||||
<property name="mode">
|
||||
<getter><![CDATA[
|
||||
if (!this._mode) {
|
||||
throw ("mode is not defined in styled-textbox.xml");
|
||||
}
|
||||
return this._mode;
|
||||
]]></getter>
|
||||
<setter><![CDATA[
|
||||
Zotero.debug("Setting mode to " + val);
|
||||
switch (val) {
|
||||
case 'note':
|
||||
var self = this;
|
||||
|
||||
this._eventHandler = function (event) {
|
||||
Zotero.debug(event.type);
|
||||
switch (event.type) {
|
||||
case 'keypress':
|
||||
// Ignore keypresses that don't change
|
||||
// any text
|
||||
if (!event.isChar &&
|
||||
event.keyCode != event.DOM_VK_DELETE &&
|
||||
event.keyCode != event.DOM_VK_BACK_SPACE) {
|
||||
//Zotero.debug("Not a char");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'change':
|
||||
Zotero.debug("Event type is " + event.type);
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (self._timer) {
|
||||
clearTimeout(self._timer);
|
||||
}
|
||||
|
||||
// Get the command event
|
||||
self._timer = self.timeout && setTimeout(function () {
|
||||
var attr = self.getAttribute('oncommand');
|
||||
attr = attr.replace('this', 'thisObj');
|
||||
var func = new Function('thisObj', 'event', attr);
|
||||
func(self, event);
|
||||
}, self.timeout);
|
||||
|
||||
return true;
|
||||
};
|
||||
break;
|
||||
|
||||
case 'integration':
|
||||
break;
|
||||
|
||||
default:
|
||||
throw ("Invalid mode '" + val + "' in styled-textbox.xml");
|
||||
}
|
||||
return this._mode = val;
|
||||
]]></setter>
|
||||
</property>
|
||||
|
||||
<!-- Sets or returns formatting (currently, HTML or Integration) of rich text box -->
|
||||
<property name="format">
|
||||
<getter><![CDATA[
|
||||
|
@ -104,30 +181,50 @@
|
|||
<!-- Sets or returns contents of rich text box -->
|
||||
<property name="value">
|
||||
<getter><![CDATA[
|
||||
this._editor.execCommand("mceCleanup");
|
||||
var body = this._editor.getBody();
|
||||
var output = body.innerHTML;
|
||||
var output = this._editor.getBody();
|
||||
output = output.innerHTML;
|
||||
Zotero.debug("RAW");
|
||||
Zotero.debug(output);
|
||||
|
||||
var output = this._editor.getContent();
|
||||
Zotero.debug("XHTML");
|
||||
Zotero.debug(output);
|
||||
|
||||
if(this._format == "Integration" || this._format == "RTF") {
|
||||
// do appropriate replacement operations
|
||||
for(var needle in this._rtfMap) {
|
||||
output = output.replace(needle, this._rtfMap[needle], "g");
|
||||
}
|
||||
|
||||
output = output.replace("<p>", "", "g");
|
||||
output = output.replace("</p>", "\\par ", "g");
|
||||
output = output.replace(/<\/?div[^>]*>/g, "");
|
||||
output = Zotero.Utilities.prototype.trim(output);
|
||||
output = Zotero.Utilities.prototype.unescapeHTML(output);
|
||||
if(output.substr(-4) == "\\par") output = output.substr(0, output.length-4);
|
||||
}
|
||||
|
||||
return output;
|
||||
]]></getter>
|
||||
<setter><![CDATA[
|
||||
Zotero.debug("Setting value!");
|
||||
|
||||
if (self._timer) {
|
||||
clearTimeout(self._timer);
|
||||
}
|
||||
|
||||
if(!this._editor) {
|
||||
// if not loaded, wait until it is to set
|
||||
return this._value = val;
|
||||
}
|
||||
|
||||
if (this.value == val) {
|
||||
Zotero.debug("Value hasn't changed!");
|
||||
return;
|
||||
}
|
||||
|
||||
Zotero.debug("Value has changed");
|
||||
|
||||
var html = val;
|
||||
|
||||
if(this._format == "Integration" || this._format == "RTF") {
|
||||
|
@ -169,11 +266,16 @@
|
|||
return val;
|
||||
]]></setter>
|
||||
</property>
|
||||
|
||||
<property name="timeout"
|
||||
onset="this.setAttribute('timeout', val); return val;"
|
||||
onget="return parseInt(this.getAttribute('timeout')) || 0;"/>
|
||||
</implementation>
|
||||
|
||||
<content>
|
||||
<xul:iframe flex="1" anonid="rt-view" class="rt-view" type="content" style="min-height:130px"
|
||||
xbl:inherits="onfocus,onblur,flex,width,height,hidden"/>
|
||||
<xul:iframe flex="1" anonid="rt-view" class="rt-view" type="content"
|
||||
xbl:inherits="onfocus,onblur,flex,width,height,hidden"
|
||||
style="overflow: hidden"/>
|
||||
</content>
|
||||
</binding>
|
||||
</bindings>
|
|
@ -111,6 +111,6 @@
|
|||
</hbox>
|
||||
</vbox>
|
||||
|
||||
<textbox id="editor" type="styled" flex="1"/>
|
||||
<textbox id="editor" type="styled" mode="integration" flex="1"/>
|
||||
</vbox>
|
||||
</dialog>
|
||||
|
|
|
@ -1,13 +1,32 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#tinymce_parent {
|
||||
display: block;
|
||||
height: 100%;
|
||||
min-height: 130px;
|
||||
}
|
||||
#tinymce_tbl {
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script type="text/javascript" src="tiny_mce.js"></script>
|
||||
<script type="text/javascript">
|
||||
tinyMCE.init({
|
||||
// General options
|
||||
mode : "none",
|
||||
theme : "advanced",
|
||||
content_css : "../../../skin/default/zotero/tinymce-content.css",
|
||||
content_css : "../../../skin/default/zotero/tinymce/integration-content.css",
|
||||
|
||||
// Theme options
|
||||
theme_advanced_buttons1 : "bold,italic,underline,|,sub,sup,|,removeformat",
|
||||
|
@ -20,7 +39,7 @@
|
|||
tinyMCE.execCommand("mceAddControl", true, "tinymce");
|
||||
</script>
|
||||
</head>
|
||||
<body style="border: 0; margin: 0;">
|
||||
<div id="tinymce" style="width:100%"></div>
|
||||
<body>
|
||||
<div id="tinymce"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -2,24 +2,79 @@
|
|||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title>TinyMCE</title>
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
#tinymce_parent {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
#tinymce_tbl {
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table.mceLayout > tbody > tr.mceLast {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 82px;
|
||||
bottom: 2px;
|
||||
left: 1px;
|
||||
right: 1px;
|
||||
}
|
||||
|
||||
td.mceIframeContainer {
|
||||
display: block;
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
#tinymce_ifr {
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript" src="tiny_mce.js"></script>
|
||||
<script type="text/javascript">
|
||||
tinyMCE.init({
|
||||
// General options
|
||||
mode : "textareas",
|
||||
mode : "none",
|
||||
theme : "advanced",
|
||||
|
||||
content_css : "../../../skin/default/zotero/tinymce/note-content.css",
|
||||
button_tile_map : true,
|
||||
language : "en", // TODO: localize
|
||||
entity_encoding : "raw",
|
||||
gecko_spellcheck : true,
|
||||
|
||||
handle_event_callback : function (event) {
|
||||
if (handleEvent) {
|
||||
handleEvent(event);
|
||||
}
|
||||
},
|
||||
|
||||
onchange_callback : function () {
|
||||
var event = { type: 'change' };
|
||||
if (handleEvent) {
|
||||
handleEvent(event);
|
||||
}
|
||||
},
|
||||
|
||||
fix_list_elements : true,
|
||||
fix_table_elements : true,
|
||||
|
||||
// Theme options
|
||||
theme_advanced_buttons1 : "bold,italic,underline,strikethrough,|,sub,sup,|,forecolor,backcolor,|,removeformat",
|
||||
theme_advanced_buttons1 : "bold,italic,underline,strikethrough,|,sub,sup,|,forecolor,backcolor,|,removeformat,code",
|
||||
theme_advanced_buttons2 : "justifyleft,justifycenter,justifyright,justifyfull,|,bullist,numlist,|,outdent,indent,blockquote,|,link,unlink",
|
||||
theme_advanced_buttons3 : "formatselect,fontselect,fontsizeselect",
|
||||
theme_advanced_toolbar_location : "top",
|
||||
theme_advanced_toolbar_align : "left",
|
||||
theme_advanced_resizing : true
|
||||
});
|
||||
tinyMCE.execCommand("mceAddControl", true, "tinymce");
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<textarea id="tinymce" rows="15" cols="80" style="width: 100%"></textarea>
|
||||
<div id="tinymce"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1240,10 +1240,16 @@ Zotero.Item.prototype.save = function() {
|
|||
sql = "INSERT INTO itemNotes "
|
||||
+ "(itemID, sourceItemID, note, title) VALUES (?,?,?,?)";
|
||||
var parent = this.isNote() ? this.getSource() : null;
|
||||
var noteText = this._noteText ? this._noteText : '';
|
||||
// Add <div> wrapper if not present
|
||||
if (!noteText.match(/^<div class="zotero\-note znv[0-9]+">.*<\/div>$/)) {
|
||||
noteText = '<div class="zotero-note znv1">' + noteText + '</div>';
|
||||
}
|
||||
|
||||
var bindParams = [
|
||||
itemID,
|
||||
parent ? parent : null,
|
||||
this._noteText ? this._noteText : '',
|
||||
noteText,
|
||||
this._noteTitle ? this._noteTitle : ''
|
||||
];
|
||||
Zotero.DB.query(sql, bindParams);
|
||||
|
@ -1575,10 +1581,15 @@ Zotero.Item.prototype.save = function() {
|
|||
sql = "REPLACE INTO itemNotes "
|
||||
+ "(itemID, sourceItemID, note, title) VALUES (?,?,?,?)";
|
||||
var parent = this.isNote() ? this.getSource() : null;
|
||||
var noteText = this._noteText;
|
||||
// Add <div> wrapper if not present
|
||||
if (!noteText.match(/^<div class="zotero\-note znv[0-9]+">.*<\/div>$/)) {
|
||||
noteText = '<div class="zotero-note znv1">' + noteText + '</div>';
|
||||
}
|
||||
var bindParams = [
|
||||
this.id,
|
||||
parent ? parent : null,
|
||||
this._noteText,
|
||||
noteText,
|
||||
this._noteTitle
|
||||
];
|
||||
Zotero.DB.query(sql, bindParams);
|
||||
|
@ -1983,6 +1994,8 @@ Zotero.Item.prototype.getNote = function() {
|
|||
|
||||
var sql = "SELECT note FROM itemNotes WHERE itemID=?";
|
||||
var note = Zotero.DB.valueQuery(sql, this.id);
|
||||
// Don't include <div> wrapper when returning value
|
||||
note = note.replace(/^<div class="zotero-note znv[0-9]+">(.*)<\/div>$/, '$1');
|
||||
|
||||
this._noteText = note ? note : '';
|
||||
|
||||
|
|
|
@ -30,6 +30,9 @@ Zotero.Notes = new function() {
|
|||
* Return first line (or first MAX_LENGTH characters) of note content
|
||||
**/
|
||||
function noteToTitle(text) {
|
||||
text = Zotero.Utilities.prototype.trim(text);
|
||||
text = Zotero.Utilities.prototype.unescapeHTML(text);
|
||||
|
||||
var max = this.MAX_TITLE_LENGTH;
|
||||
|
||||
var t = text.substring(0, max);
|
||||
|
|
|
@ -995,6 +995,20 @@ Zotero.DBConnection.prototype._getDBConnection = function () {
|
|||
};
|
||||
this._connection.createFunction('regexp', 2, rx);
|
||||
|
||||
// text2html UDF
|
||||
var rx = {
|
||||
onFunctionCall: function (arg) {
|
||||
var str = arg.getUTF8String(0);
|
||||
str = Zotero.Utilities.prototype.htmlSpecialChars(str);
|
||||
str = '<p>'
|
||||
+ str.replace(/\n/g, '</p><p>')
|
||||
.replace(/\t/g, ' ')
|
||||
.replace(/ /g, ' ')
|
||||
+ '</p>';
|
||||
return str.replace(/<p>\s*<\/p>/g, '<p> </p>');
|
||||
}
|
||||
};
|
||||
this._connection.createFunction('text2html', 1, rx);
|
||||
|
||||
return this._connection;
|
||||
}
|
||||
|
|
|
@ -70,7 +70,14 @@ Zotero.Report = new function() {
|
|||
|
||||
// Independent note
|
||||
if (arr['note']) {
|
||||
content += '<p>' + escapeXML(arr['note']) + '</p>\n';
|
||||
content += '\n';
|
||||
if (arr.note.substr(0, 1024).match(/<p[^>]*>/)) {
|
||||
content += arr.note + '\n';
|
||||
}
|
||||
// Wrap plaintext notes in <p>
|
||||
else {
|
||||
content += '<p class="plaintext">' + arr.note + '</p>\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,7 +92,15 @@ Zotero.Report = new function() {
|
|||
content += '<ul class="notes">\n';
|
||||
for each(var note in arr.reportChildren.notes) {
|
||||
content += '<li id="i' + note.itemID + '">\n';
|
||||
content += '<p>' + escapeXML(note.note) + '</p>\n';
|
||||
|
||||
content += note.note + '\n';
|
||||
if (note.note.substr(0, 1024).match(/<p[^>]*>/)) {
|
||||
content += note.note + '\n';
|
||||
}
|
||||
// Wrap plaintext notes in <p>
|
||||
else {
|
||||
content += '<p class="plaintext">' + note.note + '</p>\n';
|
||||
}
|
||||
|
||||
// Child note tags
|
||||
content += _generateTagsList(note);
|
||||
|
|
|
@ -1945,6 +1945,11 @@ Zotero.Schema = new function(){
|
|||
if (i==42) {
|
||||
Zotero.DB.query("UPDATE itemAttachments SET syncState=0");
|
||||
}
|
||||
|
||||
// 1.5 Sync Preview 2.3
|
||||
if (i==43) {
|
||||
Zotero.DB.query("UPDATE itemNotes SET note='<div class=\"zotero-note znv1\">' || TEXT2HTML(note) || '</div>' WHERE note NOT LIKE '<div class=\"zotero-note %'");
|
||||
}
|
||||
}
|
||||
|
||||
_updateDBVersion('userdata', toVersion);
|
||||
|
|
|
@ -113,12 +113,8 @@ ul.notes > li p:last-child {
|
|||
|
||||
|
||||
/* Preserve whitespace on notes */
|
||||
ul.notes li p, li.note p {
|
||||
white-space: pre-wrap; /* css-3 */
|
||||
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
|
||||
white-space: -pre-wrap; /* Opera 4-6 */
|
||||
white-space: -o-pre-wrap; /* Opera 7 */
|
||||
word-wrap: break-word; /* Internet Explorer 5.5+ */
|
||||
ul.notes li p.plaintext, li.note p.plaintext {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* Display tags within child notes inline */
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
body, td, pre {font-family:Times New Roman, Times, serif; font-size:14px; margin: 8px;}
|
||||
body, td, pre {font-family:Times New Roman, Times, serif; font-size:14px; margin: 8px;}
|
||||
p, div {margin:0; padding:0}
|
14
chrome/skin/default/zotero/tinymce/note-content.css
Normal file
14
chrome/skin/default/zotero/tinymce/note-content.css
Normal file
|
@ -0,0 +1,14 @@
|
|||
body {
|
||||
font-size: 11px;
|
||||
font-family: Lucida Grande, Tahoma, Verdana, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
/*
|
||||
blockquote p:not(:empty):before {
|
||||
content: '“'
|
||||
}
|
||||
|
||||
blockquote p:not(:empty):after {
|
||||
content: '”'
|
||||
}
|
||||
*/
|
|
@ -1,4 +1,4 @@
|
|||
-- 42
|
||||
-- 43
|
||||
|
||||
-- This file creates tables containing user-specific data -- any changes made
|
||||
-- here must be mirrored in transition steps in schema.js::_migrateSchema()
|
||||
|
|
Loading…
Reference in a new issue