summaryrefslogtreecommitdiffstats
path: root/etherpad/src/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'etherpad/src/plugins')
-rw-r--r--etherpad/src/plugins/testplugin/controllers/testplugin.js15
-rw-r--r--etherpad/src/plugins/testplugin/templates/page.ejs23
-rw-r--r--etherpad/src/plugins/testplugin/templates/testplugin.ejs10
-rw-r--r--etherpad/src/plugins/twitterStyleTags/controllers/tagBrowser.js213
-rw-r--r--etherpad/src/plugins/twitterStyleTags/hooks.js15
-rw-r--r--etherpad/src/plugins/twitterStyleTags/main.js4
-rw-r--r--etherpad/src/plugins/twitterStyleTags/models/tagQuery.js227
-rw-r--r--etherpad/src/plugins/twitterStyleTags/static/css/tagBrowser.css2
-rw-r--r--etherpad/src/plugins/twitterStyleTags/templates/tagBrowser.ejs199
-rw-r--r--etherpad/src/plugins/urlIndexer/controllers/urlBrowser.js132
-rw-r--r--etherpad/src/plugins/urlIndexer/hooks.js49
-rw-r--r--etherpad/src/plugins/urlIndexer/main.js34
-rw-r--r--etherpad/src/plugins/urlIndexer/templates/urlBrowser.ejs53
13 files changed, 644 insertions, 332 deletions
diff --git a/etherpad/src/plugins/testplugin/controllers/testplugin.js b/etherpad/src/plugins/testplugin/controllers/testplugin.js
index 0c79e06..da74ade 100644
--- a/etherpad/src/plugins/testplugin/controllers/testplugin.js
+++ b/etherpad/src/plugins/testplugin/controllers/testplugin.js
@@ -29,7 +29,6 @@ import("sqlbase.sqlbase");
import("sqlbase.sqlcommon");
import("sqlbase.sqlobj");
-
function onRequest() {
var isPro = pro_utils.isProDomainRequest();
var userId = padusers.getUserId();
@@ -47,11 +46,13 @@ function onRequest() {
var isProUser = (isPro && ! padusers.isGuest(userId));
- renderHtml("testplugin.ejs",
- {
- isPro: isPro,
- isProAccountHolder: isProUser,
- account: getSessionProAccount(), // may be falsy
- }, 'testplugin');
+ renderHtml(
+ "testplugin.ejs",
+ {
+ isPro: isPro,
+ isProAccountHolder: isProUser,
+ account: getSessionProAccount(), // may be falsy
+ },
+ 'testplugin');
return true;
}
diff --git a/etherpad/src/plugins/testplugin/templates/page.ejs b/etherpad/src/plugins/testplugin/templates/page.ejs
new file mode 100644
index 0000000..71633c0
--- /dev/null
+++ b/etherpad/src/plugins/testplugin/templates/page.ejs
@@ -0,0 +1,23 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %>
+
+<% template.define('body', function() { var ejs_data=''; %>
+ <div id="blabla">
+ <h1>Page header</h1>
+ <%: template.use('content', function() { var ejs_data=''; %>
+ Original content
+ <% return ejs_data; }); %>
+ <div>footer</div>
+ </div>
+<% return ejs_data; }); %>
diff --git a/etherpad/src/plugins/testplugin/templates/testplugin.ejs b/etherpad/src/plugins/testplugin/templates/testplugin.ejs
index f70ca8d..69c4453 100644
--- a/etherpad/src/plugins/testplugin/templates/testplugin.ejs
+++ b/etherpad/src/plugins/testplugin/templates/testplugin.ejs
@@ -24,6 +24,10 @@ limitations under the License. */ %>
helpers.addToHead('\n<style type="text/css" title="dynamicsyntax"></style>\n');
%>
-<div id="padpage">
- Welcome to the test plugin
-</div>
+<% template.inherit('page.ejs') %>
+
+<% template.define('content', function() { var ejs_data=''; %>
+ <div id="padpage">
+ Welcome to the test plugin
+ </div>
+<% return ejs_data; }); %>
diff --git a/etherpad/src/plugins/twitterStyleTags/controllers/tagBrowser.js b/etherpad/src/plugins/twitterStyleTags/controllers/tagBrowser.js
index 793067d..5335ab7 100644
--- a/etherpad/src/plugins/twitterStyleTags/controllers/tagBrowser.js
+++ b/etherpad/src/plugins/twitterStyleTags/controllers/tagBrowser.js
@@ -15,6 +15,8 @@
* limitations under the License.
*/
+import("plugins.twitterStyleTags.models.tagQuery");
+
import("faststatic");
import("dispatch.{Dispatcher,PrefixMatcher,forward}");
@@ -29,207 +31,21 @@ import("etherpad.pro.pro_accounts.getSessionProAccount");
import("sqlbase.sqlbase");
import("sqlbase.sqlcommon");
import("sqlbase.sqlobj");
+import("etherpad.pad.padutils");
-function tagsToQuery(tags, antiTags) {
- var prefixed = [];
- for (i = 0; i < antiTags.length; i++)
- prefixed[i] = '!' + antiTags[i];
- return tags.concat(prefixed).join(',');
-}
-
-function stringFormat(text, obj) {
- var name;
- for (name in obj) {
- //iterate through the params and replace their placeholders from the original text
- text = text.replace(new RegExp('%\\(' + name + '\\)s', 'gi' ), obj[name]);
- }
- return text;
-}
-
-/* All these sql query functions both takes a querySql object as
- * parameter and returns one. This object has two members - sql and
- * params. Sql is a string of an sql table name or a subqyery in
- * parens. The table pr subquery should have an ID column containing a
- * PAD_ID.
- */
-
-/* Filters pads by tags and anti-tags */
-function getQueryToSql(tags, antiTags, querySql) {
- var queryTable;
- var queryParams;
-
- if (querySql == null) {
- queryTable = 'PAD_META';
- queryParams = [];
- } else {
- queryTable = querySql.sql;
- queryParams = querySql.params;
- }
-
- var exceptArray = [];
- var joinArray = [];
- var whereArray = [];
- var exceptParamArray = [];
- var joinParamArray = [];
-
- var info = new Object();
- info.queryTable = queryTable;
- info.n = 0;
- var i;
-
- for (i = 0; i < antiTags.length; i++) {
- tag = antiTags[i];
- exceptArray.push(
- stringFormat(
- 'left join (PAD_TAG as pt%(n)s ' +
- ' join TAG AS t%(n)s on ' +
- ' t%(n)s.NAME = ? ' +
- ' and t%(n)s.ID = pt%(n)s.TAG_ID) on ' +
- ' pt%(n)s.PAD_ID = p.ID ',
- info));
- whereArray.push(stringFormat('pt%(n)s.TAG_ID is null', info));
- exceptParamArray.push(tag);
- info.n += 1;
- }
- for (i = 0; i < tags.length; i++) {
- tag = tags[i];
- joinArray.push(
- stringFormat(
- 'join PAD_TAG as pt%(n)s on ' +
- ' pt%(n)s.PAD_ID = p.ID ' +
- 'join TAG as t%(n)s on ' +
- ' t%(n)s.ID = pt%(n)s.TAG_ID ' +
- ' and t%(n)s.NAME = ? ',
- info));
- joinParamArray.push(tag);
- info.n += 1;
- }
-
- info["joins"] = joinArray.join(' ');
- info["excepts"] = exceptArray.join(' ');
- info["wheres"] = whereArray.length > 0 ? ' where ' + whereArray.join(' and ') : '';
-
- /* Create a subselect from all the joins */
- return {
- sql: stringFormat(
- '(select distinct ' +
- ' p.ID ' +
- ' from ' +
- ' %(queryTable)s as p ' +
- ' %(joins)s ' +
- ' %(excepts)s ' +
- ' %(wheres)s ' +
- ') ',
- info),
- params: queryParams.concat(joinParamArray).concat(exceptParamArray)};
-}
-
-/* Returns the sql to count the number of results from some other
- * query. */
-function nrSql(querySql) {
- var queryTable;
- var queryParams;
-
- if (querySql == null) {
- queryTable = 'PAD_META';
- queryParams = [];
- } else {
- queryTable = querySql.sql;
- queryParams = querySql.params;
- }
-
- var info = [];
- info['query_sql'] = queryTable
- return {
- sql: stringFormat('(select count(*) as total from %(query_sql)s as q)', info),
- params: queryParams};
-}
-
-/* Returns the sql to select the 10 best new tags to tack on to a
- * query, that is, the tags that are closest to halving the result-set
- * if tacked on. */
-function newTagsSql(querySql) {
- var queryTable;
- var queryParams;
-
- if (querySql == null) {
- queryTable = 'PAD_META';
- queryParams = [];
- } else {
- queryTable = querySql.sql;
- queryParams = querySql.params;
- }
-
- var info = [];
- info["query_post_table"] = queryTable;
- var queryNrSql = nrSql(querySql);
- info["query_nr_sql"] = queryNrSql.sql;
- queryNrParams = queryNrSql.params;
-
- return {
- sql: stringFormat('' +
- 'select ' +
- ' t.NAME tagname, ' +
- ' count(tp.PAD_ID) as matches, ' +
- ' tn.total - count(tp.PAD_ID) as antimatches, ' +
- ' abs(count(tp.PAD_ID) - (tn.total / 2)) as weight ' +
- 'from ' +
- ' TAG as t, ' +
- ' PAD_TAG as tp, ' +
- ' %(query_nr_sql)s as tn ' +
- 'where ' +
- ' tp.TAG_ID = t.ID ' +
- ' and tp.PAD_ID in %(query_post_table)s ' +
- ' and tp.PAD_ID NOT LIKE \'%$%\'' +
- 'group by t.NAME, tn.total ' +
- 'having ' +
- ' count(tp.PAD_ID) > 0 and count(tp.PAD_ID) < tn.total ' +
- 'order by ' +
- ' abs(count(tp.PAD_ID) - (tn.total / 2)) asc ' +
- 'limit 10 ' +
- '', info),
- params: queryNrParams.concat(queryParams)};
-}
-
-function onRequest() {
- var tags = new Array();
- var antiTags = new Array();
-
- if (request.params.query != undefined && request.params.query != '') {
- var query = request.params.query.split(',');
- for (i = 0; i < query.length; i++)
- if (query[i][0] == '!')
- antiTags.push(query[i].substring(1));
- else
- tags.push(query[i]);
- }
+function onRequest() {
+ var tags = tagQuery.queryToTags(request.params.query);
/* Create the pad filter sql */
- var querySql = getQueryToSql(tags.concat(['public']), antiTags);
+ var querySql = tagQuery.getQueryToSql(tags.tags.concat(['public']), tags.antiTags);
/* Use the pad filter sql to figure out which tags to show in the tag browser this time. */
- var queryNewTagsSql = newTagsSql(querySql);
+ var queryNewTagsSql = tagQuery.newTagsSql(querySql);
var newTags = sqlobj.executeRaw(queryNewTagsSql.sql, queryNewTagsSql.params);
- /* Select the 10 last changed matching pads and some extra information on them. Except the Pro Pads*/
- var sql = '' +
- 'select ' +
- ' m.id as ID, ' +
- ' DATE_FORMAT(m.lastWriteTime, \'%a, %d %b %Y %H:%i:%s GMT\') as lastWriteTime, ' +
- ' c.TAGS ' +
- 'from ' +
- querySql.sql + ' as q ' +
- ' join PAD_SQLMETA as m on ' +
- ' m.id = q.ID ' +
- ' join PAD_TAG_CACHE as c on ' +
- ' c.PAD_ID = q.ID ' +
- 'where ' +
- ' m.id NOT LIKE \'%$%\'' +
- 'order by ' +
- ' m.lastWriteTime desc ' +
- 'limit 10';
- var matchingPads = sqlobj.executeRaw(sql, querySql.params);
+ padSql = tagQuery.padInfoSql(querySql, 10);
+ var matchingPads = sqlobj.executeRaw(padSql.sql, padSql.params);
for (i = 0; i < matchingPads.length; i++) {
matchingPads[i].TAGS = matchingPads[i].TAGS.split('#');
@@ -251,12 +67,17 @@ function onRequest() {
var isProUser = (isPro && ! padusers.isGuest(userId));
+ padutils.setOptsAndCookiePrefs(request);
+ var prefs = helpers.getClientVar('cookiePrefsToSet');
+ var bodyClass = (prefs.isFullWidth ? "fullwidth" : "limwidth")
+
var info = {
+ prefs: prefs,
config: appjet.config,
- tagsToQuery: tagsToQuery,
+ tagQuery: tagQuery,
padIdToReadonly: server_utils.padIdToReadonly,
- tags: tags,
- antiTags: antiTags,
+ tags: tags.tags,
+ antiTags: tags.antiTags,
newTags: newTags,
matchingPads: matchingPads,
bodyClass: 'nonpropad',
diff --git a/etherpad/src/plugins/twitterStyleTags/hooks.js b/etherpad/src/plugins/twitterStyleTags/hooks.js
index 003bc32..1072579 100644
--- a/etherpad/src/plugins/twitterStyleTags/hooks.js
+++ b/etherpad/src/plugins/twitterStyleTags/hooks.js
@@ -23,10 +23,10 @@ function padModelWriteToDB(args) {
else
old_tags_str = '';
- var old_tags = old_tags_str != '' ? old_tags_str.split('#') : new Array();
+ // var old_tags = old_tags_str != '' ? old_tags_str.split('#') : new Array();
if (new_tags_str != old_tags_str) {
- log.info({message: 'Updating tags', new_tags:new_tags, old_tags:old_tags});
+ // log.info({message: 'Updating tags', new_tags:new_tags, old_tags:old_tags});
if (old_tags_row)
sqlobj.update("PAD_TAG_CACHE", {PAD_ID: args.padId }, {TAGS: new_tags.join('#')});
@@ -44,4 +44,13 @@ function padModelWriteToDB(args) {
sqlobj.insert("PAD_TAG", {PAD_ID: args.padId, TAG_ID: tag_row['ID']});
}
}
-} \ No newline at end of file
+}
+
+function docbarItemsAll() {
+ return ["<td class='docbarbutton'><a href='/ep/tag/'>Home</a></td>"];
+}
+
+function docbarItemsTagBrowser() {
+ return ["<td class='docbarbutton'><a href='/ep/tag/'>Pads</a></td>"];
+}
+
diff --git a/etherpad/src/plugins/twitterStyleTags/main.js b/etherpad/src/plugins/twitterStyleTags/main.js
index 34d5d85..d64abff 100644
--- a/etherpad/src/plugins/twitterStyleTags/main.js
+++ b/etherpad/src/plugins/twitterStyleTags/main.js
@@ -5,13 +5,15 @@ import("sqlbase.sqlobj");
import("sqlbase.sqlcommon");
function init() {
- this.hooks = ['handlePath', 'aceGetFilterStack', 'aceCreateDomLine', 'padModelWriteToDB'];
+ this.hooks = ['handlePath', 'aceGetFilterStack', 'aceCreateDomLine', 'padModelWriteToDB', 'docbarItemsAll', 'docbarItemsTagBrowser'];
this.client = new main.init();
this.description = 'Twitter-style tags allows the user to tag pads by writing #tagname anywhere in the pad text. Tags are automatically linked to searches for that tag in other pads. This plugin also provides an alternative home-page for Etherpad with a display of the last changed public pads as well as that information available as an RSS stream.';
this.handlePath = hooks.handlePath;
this.aceGetFilterStack = main.aceGetFilterStack;
this.aceCreateDomLine = main.aceCreateDomLine;
this.padModelWriteToDB = hooks.padModelWriteToDB;
+ this.docbarItemsAll = hooks.docbarItemsAll;
+ this.docbarItemsTagBrowser = hooks.docbarItemsTagBrowser;
this.install = install;
this.uninstall = uninstall;
diff --git a/etherpad/src/plugins/twitterStyleTags/models/tagQuery.js b/etherpad/src/plugins/twitterStyleTags/models/tagQuery.js
new file mode 100644
index 0000000..8a32ef7
--- /dev/null
+++ b/etherpad/src/plugins/twitterStyleTags/models/tagQuery.js
@@ -0,0 +1,227 @@
+/**
+ * Copyright 2010 RedHog, Egil Möller <egil.moller@piratpartiet.se>
+ * Copyright 2010 Pita, Peter Martischka <petermartischka@googlemail.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlbase");
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+import("etherpad.log");
+
+function tagsToQuery(tags, antiTags) {
+ var prefixed = [];
+ for (i = 0; i < antiTags.length; i++)
+ prefixed[i] = '!' + antiTags[i];
+ return tags.concat(prefixed).join(',');
+}
+
+function queryToTags(query) {
+ var tags = {
+ tags: new Array(),
+ antiTags: new Array()
+ };
+
+ if (query != undefined && query != '') {
+ var query = query.split(',');
+ for (i = 0; i < query.length; i++)
+ if (query[i][0] == '!')
+ tags.antiTags.push(query[i].substring(1));
+ else
+ tags.tags.push(query[i]);
+ }
+ return tags;
+}
+
+function stringFormat(text, obj) {
+ var name;
+ for (name in obj) {
+ //iterate through the params and replace their placeholders from the original text
+ text = text.replace(new RegExp('%\\(' + name + '\\)s', 'gi' ), obj[name]);
+ }
+ return text;
+}
+
+/* All these sql query functions both takes a querySql object as
+ * parameter and returns one. This object has two members - sql and
+ * params. Sql is a string of an sql table name or a subqyery in
+ * parens. The table pr subquery should have an ID column containing a
+ * PAD_ID.
+ */
+
+/* Filters pads by tags and anti-tags */
+function getQueryToSql(tags, antiTags, querySql) {
+ var queryTable;
+ var queryParams;
+
+ if (querySql == null) {
+ queryTable = 'PAD_META';
+ queryParams = [];
+ } else {
+ queryTable = querySql.sql;
+ queryParams = querySql.params;
+ }
+
+ var exceptArray = [];
+ var joinArray = [];
+ var whereArray = [];
+ var exceptParamArray = [];
+ var joinParamArray = [];
+
+ var info = new Object();
+ info.queryTable = queryTable;
+ info.n = 0;
+ var i;
+
+ for (i = 0; i < antiTags.length; i++) {
+ tag = antiTags[i];
+ exceptArray.push(
+ stringFormat(
+ 'left join (PAD_TAG as pt%(n)s ' +
+ ' join TAG AS t%(n)s on ' +
+ ' t%(n)s.NAME = ? ' +
+ ' and t%(n)s.ID = pt%(n)s.TAG_ID) on ' +
+ ' pt%(n)s.PAD_ID = p.ID ',
+ info));
+ whereArray.push(stringFormat('pt%(n)s.TAG_ID is null', info));
+ exceptParamArray.push(tag);
+ info.n += 1;
+ }
+ for (i = 0; i < tags.length; i++) {
+ tag = tags[i];
+ joinArray.push(
+ stringFormat(
+ 'join PAD_TAG as pt%(n)s on ' +
+ ' pt%(n)s.PAD_ID = p.ID ' +
+ 'join TAG as t%(n)s on ' +
+ ' t%(n)s.ID = pt%(n)s.TAG_ID ' +
+ ' and t%(n)s.NAME = ? ',
+ info));
+ joinParamArray.push(tag);
+ info.n += 1;
+ }
+
+ info["joins"] = joinArray.join(' ');
+ info["excepts"] = exceptArray.join(' ');
+ info["wheres"] = whereArray.length > 0 ? ' where ' + whereArray.join(' and ') : '';
+
+ /* Create a subselect from all the joins */
+ return {
+ sql: stringFormat(
+ '(select distinct ' +
+ ' p.ID ' +
+ ' from ' +
+ ' %(queryTable)s as p ' +
+ ' %(joins)s ' +
+ ' %(excepts)s ' +
+ ' %(wheres)s ' +
+ ') ',
+ info),
+ params: queryParams.concat(joinParamArray).concat(exceptParamArray)};
+}
+
+/* Returns the sql to count the number of results from some other
+ * query. */
+function nrSql(querySql) {
+ var queryTable;
+ var queryParams;
+
+ if (querySql == null) {
+ queryTable = 'PAD_META';
+ queryParams = [];
+ } else {
+ queryTable = querySql.sql;
+ queryParams = querySql.params;
+ }
+
+ var info = [];
+ info['query_sql'] = queryTable
+ return {
+ sql: stringFormat('(select count(*) as total from %(query_sql)s as q)', info),
+ params: queryParams};
+}
+
+/* Returns the sql to select the 10 best new tags to tack on to a
+ * query, that is, the tags that are closest to halving the result-set
+ * if tacked on. */
+function newTagsSql(querySql) {
+ var queryTable;
+ var queryParams;
+
+ if (querySql == null) {
+ queryTable = 'PAD_META';
+ queryParams = [];
+ } else {
+ queryTable = querySql.sql;
+ queryParams = querySql.params;
+ }
+
+ var info = [];
+ info["query_post_table"] = queryTable;
+ var queryNrSql = nrSql(querySql);
+ info["query_nr_sql"] = queryNrSql.sql;
+ queryNrParams = queryNrSql.params;
+
+ return {
+ sql: stringFormat('' +
+ 'select ' +
+ ' t.NAME tagname, ' +
+ ' count(tp.PAD_ID) as matches, ' +
+ ' tn.total - count(tp.PAD_ID) as antimatches, ' +
+ ' abs(count(tp.PAD_ID) - (tn.total / 2)) as weight ' +
+ 'from ' +
+ ' TAG as t, ' +
+ ' PAD_TAG as tp, ' +
+ ' %(query_nr_sql)s as tn ' +
+ 'where ' +
+ ' tp.TAG_ID = t.ID ' +
+ ' and tp.PAD_ID in %(query_post_table)s ' +
+ ' and tp.PAD_ID NOT LIKE \'%$%\'' +
+ 'group by t.NAME, tn.total ' +
+ 'having ' +
+ ' count(tp.PAD_ID) > 0 and count(tp.PAD_ID) < tn.total ' +
+ 'order by ' +
+ ' abs(count(tp.PAD_ID) - (tn.total / 2)) asc ' +
+ 'limit 10 ' +
+ '', info),
+ params: queryNrParams.concat(queryParams)};
+}
+
+/* Select the X last changed matching pads and some extra information
+ * on them. Except the Pro Pads*/
+function padInfoSql(querySql, limit, offset) {
+ var sql = '' +
+ 'select ' +
+ ' m.id as ID, ' +
+ ' DATE_FORMAT(m.lastWriteTime, \'%a, %d %b %Y %H:%i:%s GMT\') as lastWriteTime, ' +
+ ' c.TAGS ' +
+ 'from ' +
+ querySql.sql + ' as q ' +
+ ' join PAD_SQLMETA as m on ' +
+ ' m.id = q.ID ' +
+ ' join PAD_TAG_CACHE as c on ' +
+ ' c.PAD_ID = q.ID ' +
+ 'where ' +
+ ' m.id NOT LIKE \'%$%\'' +
+ 'order by ' +
+ ' m.lastWriteTime desc ';
+ if (limit != undefined)
+ sql += 'limit ' + limit + " ";
+ if (offset != undefined)
+ sql += 'offset ' + offset + " ";
+ return {
+ sql: sql,
+ params: querySql.params
+ };
+}
diff --git a/etherpad/src/plugins/twitterStyleTags/static/css/tagBrowser.css b/etherpad/src/plugins/twitterStyleTags/static/css/tagBrowser.css
index f3321a4..55fcda2 100644
--- a/etherpad/src/plugins/twitterStyleTags/static/css/tagBrowser.css
+++ b/etherpad/src/plugins/twitterStyleTags/static/css/tagBrowser.css
@@ -76,7 +76,7 @@ h1 {
}
#editbarinner {
- line-height: 36px;
+ line-height: 29px;
font-size: 16px;
padding-left: 6pt;
}
diff --git a/etherpad/src/plugins/twitterStyleTags/templates/tagBrowser.ejs b/etherpad/src/plugins/twitterStyleTags/templates/tagBrowser.ejs
index 1f33eb8..e101196 100644
--- a/etherpad/src/plugins/twitterStyleTags/templates/tagBrowser.ejs
+++ b/etherpad/src/plugins/twitterStyleTags/templates/tagBrowser.ejs
@@ -14,35 +14,11 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. */ %>
<%
+ template.inherit('page.ejs');
helpers.setHtmlTitle("EtherPad: Browse tags");
helpers.includeCss("plugins/twitterStyleTags/tagBrowser.css");
helpers.includeCss("plugins/twitterStyleTags/pad.css");
- helpers.setBodyId("padbody");
- helpers.addBodyClass("limwidth nonpropad nonprouser");
- helpers.includeCss("pad2_ejs.css");
- helpers.includeJs("undo-xpopup.js");
- helpers.includeCometJs();
- helpers.includeJQuery();
- helpers.includeJs("json2.js");
- helpers.includeJs("colorutils.js");
- helpers.includeJs("ace.js");
- helpers.includeJs("collab_client.js");
- helpers.includeJs("draggable.js");
- helpers.includeJs("pad_utils.js");
- helpers.includeJs("pad_cookie.js");
- helpers.includeJs("pad_editor.js");
- helpers.includeJs("pad_userlist.js");
- helpers.includeJs("pad_editbar.js");
- helpers.includeJs("pad_chat.js");
- helpers.includeJs("pad_docbar.js");
- helpers.includeJs("pad_impexp.js");
- helpers.includeJs("pad_savedrevs.js");
- helpers.includeJs("pad_connectionstatus.js");
- helpers.includeJs("pad_modals.js");
- helpers.includeJs("pad2.js");
- helpers.suppressGA();
- helpers.setRobotsPolicy({index: false, follow: false});
- helpers.addToHead('\n<link rel="alternate" href="/ep/tag/?query=' + tagsToQuery(tags, antiTags) + '&format=rss" type="application/rss+xml" title="Query results as RSS" />\n');
+ helpers.addToHead('\n<link rel="alternate" href="' + helpers.updateToUrl({format:'rss'}) + '" type="application/rss+xml" title="Query results as RSS" />\n');
function inArray(item, arr) {
for (var i = 0; i < arr.length; i++)
@@ -51,108 +27,89 @@ limitations under the License. */ %>
return false;
}
%>
+<% template.define('docBarTitle', function() { var ejs_data=''; %>
+ <td id="docbarpadtitle"><span>Browse Tags</span></td>
+<% return ejs_data; }); %>
-<div id="padpage">
- <div id="padtop">
- <div id="topbar">
- <div id="topbarleft"><!-- --></div>
- <div id="topbarright"><!-- --></div>
- <div id="topbarcenter"><a href="/" id="topbaretherpad">EtherPad</a></div>
- <% if (isProAccountHolder) { %>
- <div id="accountnav"><%= toHTML(account.email) %><a href="/ep/account/sign-out">(sign out)</a></div>
- <% } else if (isPro) { %>
- <div id="accountnav"><a href="<%= signinUrl %>">sign in</a></div>
+<%
+ template.define('docBarItems', function() {
+ return plugins.callHookStr('docbarItemsTagBrowserPad', {}, '', '', '') +
+ plugins.callHookStr('docbarItemsTagBrowser', {}, '', '', '');
+ });
+%>
+
+<% template.define('sideBar', function() { var ejs_data=''; %>
+ <div id="padusers">
+ <% if (isProAccountEnabled()) { %>
+ <a href="/ep/pad/newpad" style="padding: 25px 0" id="home-newpad">
+ Create new pad
+ </a>
+ <a href="/ep/pro-signup/" style="padding: 25px 0" id="home-newteam">
+ Create new team
+ </a>
+ <% } else { %>
+ <a href="/ep/pad/newpad" id="home-newpad">
+ Create new pad
+ </a>
<% } %>
</div>
- <div id="docbar">
- <table border="0" cellpadding="0" cellspacing="0" width="100%" id="docbartable">
- <td><img src="/static/img/jun09/pad/roundcorner_left.gif"></td>
- <td id="docbarpadtitle"><span>Browse Tags</span></td>
- <td width="100%">&nbsp;</td>
- <td><img src="/static/img/jun09/pad/roundcorner_right.gif"></td>
- </table>
- </div>
- <div id="padmain">
- <div id="padsidebar">
- <div id="padusers">
- <% if (isProAccountEnabled()) { %>
- <a href="/ep/pad/newpad" style="padding: 25px 0" id="home-newpad">
- Create new pad
- </a>
- <a href="/ep/pro-signup/" style="padding: 25px 0" id="home-newteam">
- Create new team
- </a>
- <% } else { %>
- <a href="/ep/pad/newpad" id="home-newpad">
- Create new pad
- </a>
- <% } %>
- </div>
+ <div id="hdraggie"><!-- --></div>
- <div id="hdraggie"><!-- --></div>
+ <div id="padchat"><iframe src="<%= config['motdPage'] %>" width="100%" height="100%"></iframe></div>
+<% return ejs_data; }); %>
- <div id="padchat"><iframe src="<%= config['motdPage'] %>" width="100%" height="100%"></iframe></div>
- </div> <!-- /padsidebar -->
-
- <div id="padeditor">
- <div id="editbar" class="enabledtoolbar">
- <div id="editbarleft"><!-- --></div>
- <div id="editbarright"><!-- --></div>
-
- <div id="editbarinner">
- Query:
- <% if (tags.length == 0 && antiTags.length == 0) { %>
- Latest changed pads
- <% } else { %>
- <% for (i = 0; i < tags.length; i++) { %>
- <a href="/ep/tag/?query=<%= tagsToQuery(tags.filter(function (tag) { return tag != tags[i]}), antiTags) %>" class="padtag" title="<%= tags[i] %> matches">#<%= tags[i] %></a>
- <% } %>
- <% for (i = 0; i < antiTags.length; i++) { %>
- <a href="/ep/tag/?query=<%= tagsToQuery(tags, antiTags.filter(function (tag) { return tag != antiTags[i]})) %>" class="anti_padtag" title="<%= antiTags[i] %> matches">!#<%= antiTags[i] %></a>
- <% } %>
- <% } %>
- </div>
- </div>
- <div id="editorcontainerbox">
- <div id="editorcontainer">
- <div class="query-refiner">
- <h1>Search for pads that have the tag</h1>
- <% for (i = 0; i < newTags.length; i++) { %>
- <a href="/ep/tag/?query=<%= tagsToQuery(tags.concat([newTags[i].tagname]),antiTags) %>" class="padtag" title="<%= newTags[i].matches %> matches">#<%= newTags[i].tagname %></a>
- <% } %>
+<% template.define('editBarItemsLeft', function() { var ejs_data=''; %>
+ <td>
+ Query:
+ <% if (tags.length == 0 && antiTags.length == 0) { %>
+ Latest changed pads
+ <% } else { %>
+ <% for (i = 0; i < tags.length; i++) { %>
+ <a href="<%= helpers.updateToUrl({query:tagQuery.tagsToQuery(tags.filter(function (tag) { return tag != tags[i]}), antiTags)}) %>" class="padtag" title="<%= tags[i] %> matches">#<%= tags[i] %></a>
+ <% } %>
+ <% for (i = 0; i < antiTags.length; i++) { %>
+ <a href="<%= helpers.updateToUrl({query:tagQuery.tagsToQuery(tags, antiTags.filter(function (tag) { return tag != antiTags[i]}))}) %>" class="anti_padtag" title="<%= antiTags[i] %> matches">!#<%= antiTags[i] %></a>
+ <% } %>
+ <% } %>
+ </td>
+<% return ejs_data; }); %>
- <h1>Search for pads that <em>don't</em> have the tag</h1>
- <% for (i = 0; i < newTags.length; i++) { %>
- <a href="/ep/tag/?query=<%= tagsToQuery(tags,antiTags.concat([newTags[i].tagname])) %>" class="anti_padtag" title="<%= newTags[i].antimatches %> matches">!#<%= newTags[i].tagname %></a>
- <% } %>
- </div>
+<% template.define('contentArea', function() { var ejs_data=''; %>
+ <div id="editorcontainer">
+ <div class="query-refiner">
+ <%: template.use('queryRefiner', function() { var ejs_data=''; %>
+ <h1>Search for pads that have the tag</h1>
+ <% for (i = 0; i < newTags.length; i++) { %>
+ <a href="<%= helpers.updateToUrl({query:tagQuery.tagsToQuery(tags.concat([newTags[i].tagname]),antiTags)}) %>" class="padtag" title="<%= newTags[i].matches %> matches">#<%= newTags[i].tagname %></a>
+ <% } %>
- <dl>
- <% for (i = 0; i < matchingPads.length; i++) { %>
- <%
- var matchingPadId = matchingPads[i].ID;
- var matchingPadUrl = matchingPadId;
- if (!inArray('writable', matchingPads[i].TAGS)) {
- matchingPadId = padIdToReadonly(matchingPads[i].ID);
- matchingPadUrl = 'ep/pad/view/' + matchingPadId + '/latest';
- }
- %>
- <dt><a href="/<%= matchingPadUrl %>"><%= matchingPadId %></a><dt>
- <dd>
- <% for (j = 0; j < matchingPads[i].TAGS.length; j++) { %>
- <a href="/ep/tag/?query=<%= tagsToQuery(tags.concat([matchingPads[i].TAGS[j]]), antiTags) %>" class="padtag" title="<%= matchingPads[i].TAGS[j] %> matches">#<%= matchingPads[i].TAGS[j] %></a>
- <% } %>
- </dd>
- <% } %>
- </dl>
- </div>
- </div>
- </div><!-- /padeditor -->
+ <h1>Search for pads that <em>don't</em> have the tag</h1>
+ <% for (i = 0; i < newTags.length; i++) { %>
+ <a href="<%= helpers.updateToUrl({query:tagQuery.tagsToQuery(tags,antiTags.concat([newTags[i].tagname]))}) %>" class="anti_padtag" title="<%= newTags[i].antimatches %> matches">!#<%= newTags[i].tagname %></a>
+ <% } %>
+ <% return ejs_data; }); %>
+ </div>
- <div id="bottomarea">
- <div id="widthprefcheck" class="widthprefunchecked"><!-- --></div>
- <div id="sidebarcheck" class="sidebarchecked"><!-- --></div>
+ <dl>
+ <%: template.use('queryResult', function() { var ejs_data=''; %>
+ <% for (i = 0; i < matchingPads.length; i++) { %>
+ <%
+ var matchingPadId = matchingPads[i].ID;
+ var matchingPadUrl = matchingPadId;
+ if (!inArray('writable', matchingPads[i].TAGS)) {
+ matchingPadId = padIdToReadonly(matchingPads[i].ID);
+ matchingPadUrl = 'ep/pad/view/' + matchingPadId + '/latest';
+ }
+ %>
+ <dt><a href="/<%= matchingPadUrl %>"><%= matchingPadId %></a><dt>
+ <dd>
+ <% for (j = 0; j < matchingPads[i].TAGS.length; j++) { %>
+ <a href="<%= helpers.updateToUrl({query:tagQuery.tagsToQuery(tags.concat([matchingPads[i].TAGS[j]]), antiTags)}) %>" class="padtag" title="<%= matchingPads[i].TAGS[j] %> matches">#<%= matchingPads[i].TAGS[j] %></a>
+ <% } %>
+ </dd>
+ <% } %>
+ <% return ejs_data; }); %>
+ </dl>
</div>
- </div>
-</div>
+<% return ejs_data; }); %>
diff --git a/etherpad/src/plugins/urlIndexer/controllers/urlBrowser.js b/etherpad/src/plugins/urlIndexer/controllers/urlBrowser.js
new file mode 100644
index 0000000..cdb9602
--- /dev/null
+++ b/etherpad/src/plugins/urlIndexer/controllers/urlBrowser.js
@@ -0,0 +1,132 @@
+/**
+ * Copyright 2009 RedHog, Egil Möller <egil.moller@piratpartiet.se>
+ * Copyright 2010 Pita, Peter Martischka <petermartischka@googlemail.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("plugins.twitterStyleTags.models.tagQuery");
+
+import("faststatic");
+import("dispatch.{Dispatcher,PrefixMatcher,forward}");
+
+import("etherpad.utils.*");
+import("etherpad.collab.server_utils");
+import("etherpad.globals.*");
+import("etherpad.log");
+import("etherpad.pad.padusers");
+import("etherpad.pro.pro_utils");
+import("etherpad.helpers");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("sqlbase.sqlbase");
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+import("etherpad.pad.padutils");
+
+function urlSql(querySql, limit, offset) {
+ var sql = '' +
+ 'select ' +
+ ' u.URL, ' +
+ ' m.id as ID, ' +
+ ' DATE_FORMAT(m.lastWriteTime, \'%a, %d %b %Y %H:%i:%s GMT\') as lastWriteTime, ' +
+ ' c.TAGS ' +
+ 'from ' +
+ querySql.sql + ' as q ' +
+ ' join PAD_SQLMETA as m on ' +
+ ' m.id = q.ID ' +
+ ' join PAD_TAG_CACHE as c on ' +
+ ' c.PAD_ID = q.ID ' +
+ ' join PAD_URL as u on ' +
+ ' u.PAD_ID = q.ID ' +
+ 'where ' +
+ ' m.id NOT LIKE \'%$%\'' +
+ 'order by ' +
+ ' u.URL asc ';
+ if (limit != undefined)
+ sql += 'limit ' + limit + " ";
+ if (offset != undefined)
+ sql += 'offset ' + offset + " ";
+ return {
+ sql: sql,
+ params: querySql.params
+ };
+}
+
+function onRequest() {
+ var tags = tagQuery.queryToTags(request.params.query);
+
+ /* Create the pad filter sql */
+ var querySql = tagQuery.getQueryToSql(tags.tags.concat(['public']), tags.antiTags);
+
+ /* Use the pad filter sql to figure out which tags to show in the tag browser this time. */
+ var queryNewTagsSql = tagQuery.newTagsSql(querySql);
+ var newTags = sqlobj.executeRaw(queryNewTagsSql.sql, queryNewTagsSql.params);
+
+ url = urlSql(querySql, 10);
+ var matchingUrls = sqlobj.executeRaw(url.sql, url.params);
+
+ for (i = 0; i < matchingUrls.length; i++) {
+ matchingUrls[i].TAGS = matchingUrls[i].TAGS.split('#');
+ }
+
+ var isPro = pro_utils.isProDomainRequest();
+ var userId = padusers.getUserId();
+
+ helpers.addClientVars({
+ userAgent: request.headers["User-Agent"],
+ debugEnabled: request.params.djs,
+ clientIp: request.clientAddr,
+ colorPalette: COLOR_PALETTE,
+ serverTimestamp: +(new Date),
+ isProPad: isPro,
+ userIsGuest: padusers.isGuest(userId),
+ userId: userId,
+ });
+
+ var isProUser = (isPro && ! padusers.isGuest(userId));
+
+ padutils.setOptsAndCookiePrefs(request);
+ var prefs = helpers.getClientVar('cookiePrefsToSet');
+ var bodyClass = (prefs.isFullWidth ? "fullwidth" : "limwidth")
+
+ var info = {
+ prefs: prefs,
+ config: appjet.config,
+ tagQuery: tagQuery,
+ padIdToReadonly: server_utils.padIdToReadonly,
+ tags: tags.tags,
+ antiTags: tags.antiTags,
+ newTags: newTags,
+ matchingPads: [],
+ matchingUrls: matchingUrls,
+ bodyClass: 'nonpropad',
+ isPro: isPro,
+ isProAccountHolder: isProUser,
+ account: getSessionProAccount(), // may be falsy
+ };
+
+ var format = "html";
+ if (request.params.format != undefined)
+ format = request.params.format;
+
+ if (format == "html")
+ renderHtml("urlBrowser.ejs", info, ['urlIndexer', 'twitterStyleTags']);
+ else if (format == "rss") {
+ response.setContentType("application/xml; charset=utf-8");
+ response.write(renderTemplateAsString("tagRss.ejs", info, 'urlIndexer'));
+ if (request.acceptsGzip) {
+ response.setGzip(true);
+ }
+ }
+ return true;
+}
diff --git a/etherpad/src/plugins/urlIndexer/hooks.js b/etherpad/src/plugins/urlIndexer/hooks.js
new file mode 100644
index 0000000..e0ff050
--- /dev/null
+++ b/etherpad/src/plugins/urlIndexer/hooks.js
@@ -0,0 +1,49 @@
+import("etherpad.log");
+import("dispatch.{Dispatcher,PrefixMatcher,forward}");
+import("sqlbase.sqlobj");
+import("plugins.urlIndexer.controllers.urlBrowser");
+
+function handlePath() {
+ return [[PrefixMatcher('/ep/url'), forward(urlBrowser)]];
+}
+
+REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/;
+REGEX_URLCHAR = new RegExp('('+/[-:@a-zA-Z0-9_.,~%+\/\\?=&#;()$]/.source+'|'+REGEX_WORDCHAR.source+')');
+REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:)/.source+REGEX_URLCHAR.source+'*(?![:.,;])'+REGEX_URLCHAR.source, 'g');
+
+function padModelWriteToDB(args) {
+ /* Update tags for the pad */
+
+ var new_urls = args.pad.text().match(REGEX_URL);
+ if (new_urls == null) new_urls = new Array();
+ var new_urls_str = new_urls.join(' ')
+
+ var old_urls_row = sqlobj.selectSingle("PAD_URL_CACHE", { PAD_ID: args.padId });
+ var old_urls_str;
+ if (old_urls_row !== null)
+ old_urls_str = old_urls_row['URLS'];
+ else
+ old_urls_str = '';
+
+ // var old_urls = old_urls_str != '' ? old_urls_str.split(' ') : new Array();
+
+ if (new_urls_str != old_urls_str) {
+ // log.info({message: 'Updating urls', new_urls:new_urls, old_urls:old_urls});
+
+ if (old_urls_row)
+ sqlobj.update("PAD_URL_CACHE", {PAD_ID: args.padId }, {URLS: new_urls.join(' ')});
+ else
+ sqlobj.insert("PAD_URL_CACHE", {PAD_ID: args.padId, URLS: new_urls.join(' ')});
+
+ sqlobj.deleteRows("PAD_URL", {PAD_ID: args.padId});
+
+ for (i = 0; i < new_urls.length; i++) {
+ sqlobj.insert("PAD_URL", {PAD_ID: args.padId, URL: new_urls[i]});
+ }
+ }
+}
+
+function docbarItemsTagBrowser() {
+ return ["<td class='docbarbutton'><a href='/ep/url/'>URLs</a></td>"];
+}
+
diff --git a/etherpad/src/plugins/urlIndexer/main.js b/etherpad/src/plugins/urlIndexer/main.js
new file mode 100644
index 0000000..18bdef1
--- /dev/null
+++ b/etherpad/src/plugins/urlIndexer/main.js
@@ -0,0 +1,34 @@
+import("etherpad.log");
+import("plugins.urlIndexer.hooks");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+
+function init() {
+ this.hooks = ['padModelWriteToDB', 'handlePath', 'docbarItemsTagBrowser'];
+ this.description = 'Indexes URLs linked to in pads so that they can be displayed outside pads, searched for etc.';
+ this.padModelWriteToDB = hooks.padModelWriteToDB;
+ this.handlePath = hooks.handlePath;
+ this.docbarItemsTagBrowser = hooks.docbarItemsTagBrowser;
+
+ this.install = install;
+ this.uninstall = uninstall;
+}
+
+function install() {
+ log.info("Installing urlIndexer");
+
+ sqlobj.createTable('PAD_URL', {
+ PAD_ID: 'varchar(128) character set utf8 collate utf8_bin not null references PAD_META(ID)',
+ URL: 'varchar(1024) character set utf8 collate utf8_bin not null',
+ });
+
+ sqlobj.createTable('PAD_URL_CACHE', {
+ PAD_ID: 'varchar(128) character set utf8 collate utf8_bin unique not null references PAD_META(ID)',
+ URLS: 'text collate utf8_bin not null',
+ });
+}
+
+function uninstall() {
+ log.info("Uninstalling urlIndexer");
+}
+
diff --git a/etherpad/src/plugins/urlIndexer/templates/urlBrowser.ejs b/etherpad/src/plugins/urlIndexer/templates/urlBrowser.ejs
new file mode 100644
index 0000000..1996dc5
--- /dev/null
+++ b/etherpad/src/plugins/urlIndexer/templates/urlBrowser.ejs
@@ -0,0 +1,53 @@
+<% /*
+Copyright 2009 Google Inc.
+Copyright 2010 Pita, Peter Martischka <petermartischka@googlemail.com>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %>
+<%
+ template.inherit('tagBrowser.ejs');
+ helpers.setHtmlTitle("EtherPad: Browse URLs by tags");
+ helpers.includeCss("plugins/twitterStyleTags/tagBrowser.css");
+ helpers.includeCss("plugins/twitterStyleTags/pad.css");
+ helpers.addToHead('\n<link rel="alternate" href="' + helpers.updateToUrl({format:'rss'}) + '" type="application/rss+xml" title="Query results as RSS" />\n');
+
+ function inArray(item, arr) {
+ for (var i = 0; i < arr.length; i++)
+ if (arr[i] == item)
+ return true;
+ return false;
+ }
+%>
+
+<% template.define('docBarTitle', function() { var ejs_data=''; %>
+ <td id="docbarpadtitle"><span>Browse URLs by tags</span></td>
+<% return ejs_data; }); %>
+
+<% template.define('queryResult', function() { var ejs_data=''; %>
+ <% for (i = 0; i < matchingUrls.length; i++) { %>
+ <%
+ var matchingPadId = matchingUrls[i].ID;
+ var matchingPadUrl = matchingPadId;
+ if (!inArray('writable', matchingUrls[i].TAGS)) {
+ matchingPadId = padIdToReadonly(matchingUrls[i].ID);
+ matchingPadUrl = 'ep/pad/view/' + matchingPadId + '/latest';
+ }
+ %>
+ <dt><a href="<%= matchingUrls[i].URL %>"><%= matchingUrls[i].URL %></a><dt>
+ <dd>
+ <a href="<%= matchingPadUrl %>"><%= matchingPadId %></a>:
+ <% for (j = 0; j < matchingUrls[i].TAGS.length; j++) { %>
+ <a href="<%= helpers.updateToUrl({query:tagQuery.tagsToQuery(tags.concat([matchingUrls[i].TAGS[j]]), antiTags)}) %>" class="padtag" title="<%= matchingUrls[i].TAGS[j] %> matches">#<%= matchingUrls[i].TAGS[j] %></a>
+ <% } %>
+ </dd>
+ <% } %>
+<% return ejs_data; }); %>