├── .gitignore
├── .npmignore
├── README.md
├── demo
├── index.html
├── locations.xls
├── mapsheet.js
└── tabletop.js
├── package.json
└── server.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | demo/
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # simplegdocserver
2 |
3 | Simple REST Server that emulates Google Docs interface using your Excel files.
4 | (currently read-only)
5 |
6 | ## Installation
7 |
8 | (requires node + npm)
9 |
10 | ```
11 | npm install -g simplegdocserver
12 | ```
13 |
14 | ## Usage
15 |
16 | To host files from the current directory, on port 7263:
17 |
18 | ```
19 | $ simplegdocserver
20 | ```
21 |
22 | To use a different port, either set the PORT environment variable or pass it
23 | as an argument:
24 |
25 | ```
26 | $ simplegdocserver 8000
27 | ```
28 |
29 | To host from a different directory, pass a third argument or set the BASE_DIR
30 | environment variable:
31 |
32 | ```
33 | $ simplegdocserver 8000 ./test_files
34 | ```
35 |
36 | ## Interaction with Client Libraries
37 |
38 | Using [tabletop](https://github.com/jsoma/tabletop), just set the endpoint:
39 |
40 | ```
41 | Tabletop.init( {
42 | endpoint:"http://localhost:7263", // <-- adjust based on server settings
43 | key: "myfile.xls", // <-- the actual filename
44 | ...
45 | });
46 | ```
47 |
48 | The demo directory includes a demo using [mapsheet](https://github.com/jsoma/mapsheet) to annotate a map.
49 |
50 | ## Notes
51 |
52 | *there is no caching*. This is intentional: you can quickly change your data
53 | in Excel and simplegdocserver will immediately see the change.
54 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
22 |
23 |
25 |
30 |
31 |
32 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/demo/locations.xls:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SheetJS/sgds/5664504616ad62651856840897f8b1c99d69162a/demo/locations.xls
--------------------------------------------------------------------------------
/demo/mapsheet.js:
--------------------------------------------------------------------------------
1 | (function(global) {
2 | "use strict";
3 |
4 | function merge_options(obj1, obj2) {
5 | var obj3 = {};
6 | var attrname;
7 | for (attrname in obj1) { obj3[attrname] = obj1[attrname]; }
8 | for (attrname in obj2) { obj3[attrname] = obj2[attrname]; }
9 | return obj3;
10 | }
11 |
12 | var Mapsheet = global.Mapsheet = function(options) {
13 | // Make sure Mapsheet is being used as a constructor no matter what.
14 | if(!this || !(this instanceof Mapsheet)) {
15 | return new Mapsheet(options);
16 | }
17 |
18 | this.key = options.key;
19 | this.click = options.click;
20 | this.passedMap = options.map;
21 | this.element = options.element;
22 | this.sheetName = options.sheetName;
23 | this.provider = options.provider || Mapsheet.Providers.Google;
24 | this.renderer = new this.provider( { map: options.passedMap, mapOptions: options.mapOptions, layerOptions: options.layerOptions, markerLayer: options.markerLayer } );
25 | this.fields = options.fields;
26 | this.titleColumn = options.titleColumn;
27 | this.popupContent = options.popupContent;
28 | this.popupTemplate = options.popupTemplate;
29 | this.callbackContext = options.callbackContext;
30 | this.callback = options.callback;
31 |
32 | // Let's automatically engage simpleSheet mode,
33 | // which allows for easier using of data later on
34 | // if you have multiple sheets, you'll want to
35 | // disable this
36 | var simpleSheet = true;
37 |
38 | if(typeof(this.popupTemplate) === 'string') {
39 | var source = document.getElementById(this.popupTemplate).innerHTML;
40 | this.popupTemplate = Handlebars.compile(source);
41 | }
42 | this.markerOptions = options.markerOptions || {};
43 |
44 | if(typeof(this.element) === 'string') {
45 | this.element = document.getElementById(this.element);
46 | };
47 |
48 | this.tabletop = new Tabletop( { key: this.key, callback: this.loadPoints, callbackContext: this, simpleSheet: simpleSheet, proxy: options.proxy, endpoint: options.endpoint } );
49 | };
50 |
51 |
52 | Mapsheet.prototype = {
53 |
54 | loadPoints: function(data, tabletop) {
55 | this.points = [];
56 |
57 | if(typeof(this.sheetName) === 'undefined') {
58 | this.sheetName = tabletop.model_names[0];
59 | }
60 |
61 | var elements = tabletop.sheets(this.sheetName).elements;
62 |
63 | for(var i = 0; i < elements.length; i++) {
64 | var point = new Mapsheet.Point( { model: elements[i], fields: this.fields, popupContent: this.popupContent, popupTemplate: this.popupTemplate, markerOptions: this.markerOptions, titleColumn: this.titleColumn, click: this.click } );
65 | this.points.push(point);
66 | };
67 |
68 | this.draw();
69 | },
70 |
71 | draw: function() {
72 | this.renderer.initialize(this.element);
73 | this.renderer.drawPoints(this.points);
74 | if(this.callback) {
75 | this.callback.apply(this.callbackContext || this, [this, this.tabletop]);
76 | }
77 | },
78 |
79 | log: function(msg) {
80 | if(this.debug) {
81 | if(typeof console !== "undefined" && typeof console.log !== "undefined") {
82 | Function.prototype.apply.apply(console.log, [console, arguments]);
83 | }
84 | }
85 | },
86 |
87 | map: function() {
88 | return (this.passedMap || this.renderer.map);
89 | }
90 |
91 | };
92 |
93 | Mapsheet.Point = function(options) {
94 | this.model = options.model;
95 | this.fields = options.fields;
96 | this.popupContent = options.popupContent;
97 | this.popupTemplate = options.popupTemplate;
98 | this.titleColumn = options.titleColumn;
99 | this.markerOptions = options.markerOptions;
100 | this.click = options.click
101 | };
102 |
103 | Mapsheet.Point.prototype = {
104 | coords: function() {
105 | return [ this.latitude(), this.longitude() ];
106 | },
107 |
108 | latitude: function() {
109 | return parseFloat( this.model["latitude"] || this.model["lat"] );
110 | },
111 |
112 | longitude: function() {
113 | return parseFloat( this.model["longitude"] || this.model["lng"] || this.model["long"] );
114 | },
115 |
116 | get: function(fieldName) {
117 | if(typeof(fieldName) === 'undefined') {
118 | return;
119 | }
120 | return this.model[fieldName.toLowerCase().replace(/ +/,'')];
121 | },
122 |
123 | title: function() {
124 | return this.get(this.titleColumn);
125 | },
126 |
127 | isValid: function() {
128 | return !isNaN( this.latitude() ) && !isNaN( this.longitude() )
129 | },
130 |
131 | content: function() {
132 | var html = "";
133 | if(typeof(this.popupContent) !== 'undefined') {
134 | html = this.popupContent.call(this, this.model);
135 | } else if(typeof(this.popupTemplate) !== 'undefined') {
136 | html = this.popupTemplate.call(this, this.model);
137 | } else if(typeof(this.fields) !== 'undefined') {
138 | if(typeof(this.title()) !== 'undefined' && this.title() !== '') {
139 | html += "" + this.title() + "
";
140 | }
141 | for(var i = 0; i < this.fields.length; i++) {
142 | html += "" + this.fields[i] + ": " + this.get(this.fields[i]) + "
";
143 | }
144 | } else {
145 | return '';
146 | }
147 | return ""
148 | }
149 | };
150 |
151 | /*
152 |
153 | Providers only need respond to initialize and drawPoints
154 |
155 | */
156 |
157 | Mapsheet.Providers = {};
158 |
159 |
160 | /*
161 |
162 | Google Maps
163 |
164 | */
165 |
166 | Mapsheet.Providers.Google = function(options) {
167 | this.map = options.map;
168 | this.mapOptions = merge_options( { mapTypeId: google.maps.MapTypeId.ROADMAP }, options.mapOptions || {} );
169 | // We'll be nice and allow center to be a lat/lng array instead of a Google Maps LatLng
170 | if(this.mapOptions.center && this.mapOptions.center.length == 2) {
171 | this.mapOptions.center = new google.maps.LatLng(this.mapOptions.center[0], this.mapOptions.center[1]);
172 | }
173 | };
174 |
175 | Mapsheet.Providers.Google.prototype = {
176 | initialize: function(element) {
177 | if(typeof(this.map) === 'undefined') {
178 | this.map = new google.maps.Map(element, this.mapOptions);
179 | }
180 | this.bounds = new google.maps.LatLngBounds();
181 | this.infowindow = new google.maps.InfoWindow({ content: "loading...", maxWidth: '300' });
182 | },
183 |
184 | /*
185 | Google Maps only colors markers #FE7569, but turns out you can use
186 | the Google Charts API to make markers any hex color! Amazing.
187 |
188 | This code was pulled from
189 | http://stackoverflow.com/questions/7095574/google-maps-api-3-custom-marker-color-for-default-dot-marker/7686977#7686977
190 | */
191 |
192 | setMarkerIcon: function(marker) {
193 | if(typeof(marker.point.get('icon url')) !== 'undefined' && marker.point.get('icon url') !== '') {
194 | marker.setIcon(marker.point.get('icon url'));
195 | return;
196 | };
197 |
198 | if(typeof(marker.point.markerOptions['iconUrl']) !== 'undefined' && marker.point.markerOptions['iconUrl'] !== '') {
199 | marker.setIcon( marker.point.markerOptions['iconUrl']);
200 | return;
201 | };
202 |
203 | var pinColor = marker.point.get('hexcolor') || "FE7569";
204 | pinColor = pinColor.replace('#','');
205 |
206 | var pinImage = new google.maps.MarkerImage("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=|" + pinColor,
207 | new google.maps.Size(21, 34),
208 | new google.maps.Point(0,0),
209 | new google.maps.Point(10, 34));
210 | var pinShadow = new google.maps.MarkerImage("http://chart.apis.google.com/chart?chst=d_map_pin_shadow",
211 | new google.maps.Size(40, 37),
212 | new google.maps.Point(0, 0),
213 | new google.maps.Point(12, 35));
214 | marker.setShadow(pinShadow);
215 | marker.setIcon(pinImage);
216 | },
217 |
218 | drawMarker: function(point) {
219 | var latLng = new google.maps.LatLng(point.latitude(), point.longitude());
220 |
221 | var marker = new google.maps.Marker({
222 | position: latLng,
223 | point: point,
224 | title: point.title()
225 | });
226 |
227 | this.setMarkerIcon(marker);
228 | this.initInfoWindow(marker);
229 |
230 | if(point.click) {
231 | google.maps.event.addListener(marker, 'click', function(e) {
232 | point.click.call(this, e, point);
233 | });
234 | }
235 |
236 | return marker;
237 | },
238 |
239 | /*
240 | Google only lets you draw one InfoWindow on the page, so you
241 | end up having to re-write to the original one each time.
242 | */
243 |
244 | initInfoWindow: function(marker) {
245 | var infowindow = this.infowindow;
246 | var clickedOpen = false;
247 |
248 | // All of the extra blah blahs are for making sure to not repopulate
249 | // the infowindow when it's already opened and populated with the
250 | // right content
251 |
252 | google.maps.event.addListener(marker, 'click', function() {
253 | if(infowindow.getAnchor() === marker && infowindow.opened) {
254 | return;
255 | }
256 | infowindow.setContent(this.point.content());
257 | infowindow.open(this.map, this);
258 | clickedOpen = true;
259 | infowindow.opened = true;
260 | });
261 |
262 | },
263 |
264 | drawPoints: function(points) {
265 | for(var i = 0; i < points.length; i++) {
266 | if(!points[i].isValid()) { continue; }
267 | var marker = this.drawMarker(points[i]);
268 | marker.setMap(this.map);
269 | this.bounds.extend(marker.position);
270 | points[i].marker = marker;
271 | };
272 |
273 | if(!this.mapOptions.zoom && !this.mapOptions.center) {
274 | this.map.fitBounds(this.bounds);
275 | }
276 | }
277 | }
278 |
279 | /*
280 |
281 | MapQuest (OpenStreetMaps & free)
282 |
283 | */
284 |
285 | Mapsheet.Providers.MapQuest = function(options) {
286 | this.map = options.map;
287 | this.mapOptions = merge_options({ mapTypeId: 'osm', zoom: 13, bestFitMargin: 0, zoomOnDoubleClick: true, latLng:{lat:40.735383, lng:-73.984655} }, options.mapOptions || {});
288 | };
289 |
290 | Mapsheet.Providers.MapQuest.prototype = {
291 | initialize: function(element) {
292 | if(typeof(this.map) === 'undefined') {
293 | this.map = new MQA.TileMap( merge_options({ elt: element }, this.mapOptions) );
294 | }
295 | },
296 |
297 | // We need custom icons!
298 |
299 | drawMarker: function(point) {
300 | var marker = new MQA.Poi( { lat: point.latitude(), lng: point.longitude() } );
301 |
302 | marker.setRolloverContent(point.title());
303 | marker.setInfoContentHTML(point.content());
304 |
305 | if(point.click) {
306 | MQA.EventManager.addListener(marker, 'click', function(e) {
307 | point.click.call(this, e, point);
308 | });
309 | }
310 |
311 | return marker;
312 | },
313 |
314 | drawPoints: function(points) {
315 | for(var i = 0; i < points.length; i++) {
316 | if(!points[i].isValid()) { continue; }
317 | var marker = this.drawMarker(points[i]);
318 | this.map.addShape(marker);
319 | points[i].marker = marker;
320 | };
321 | this.map.bestFit();
322 | }
323 | }
324 |
325 | /*
326 |
327 | MapBox
328 |
329 | */
330 |
331 | Mapsheet.Providers.MapBox = function(options) {
332 | this.map = options.map;
333 | this.mapOptions = merge_options({ mapId: 'examples.map-vyofok3q'}, options.mapOptions || {});
334 | this.markerLayer = options.markerLayer || L.mapbox.markerLayer();
335 | this.bounds = new L.LatLngBounds();
336 | };
337 |
338 | Mapsheet.Providers.MapBox.prototype = {
339 | initialize: function(element) {
340 | if(typeof(this.map) === 'undefined') {
341 | this.map = L.mapbox.map( element );
342 | this.map.addLayer(L.mapbox.tileLayer(this.mapOptions['mapId'])); // add the base layer
343 | // this.map.ui.zoomer.add();
344 | // this.map.ui.zoombox.add();
345 | }
346 | },
347 |
348 | drawMarker: function(point) {
349 | var marker = L.marker(point.coords())
350 | .bindPopup(point.content())
351 |
352 | if(typeof(point.get('icon url')) !== 'undefined' && point.get('icon url') !== '') {
353 | var options = merge_options( point.markerOptions, { iconUrl: point.get('icon url') } );
354 | var icon = L.icon(options);
355 | marker.setIcon(icon);
356 | } else if(typeof(point.markerOptions['iconUrl']) !== 'undefined') {
357 | var icon = L.icon(point.markerOptions);
358 | marker.setIcon(icon);
359 | }
360 |
361 | if(point.click) {
362 | marker.on('click', function(e) {
363 | point.click.call(this, e, point);
364 | });
365 | }
366 | // var icon = L.icon();
367 | // marker.setIcon(icon);
368 |
369 | return marker;
370 | },
371 |
372 | drawPoints: function(points) {
373 | for(var i = 0; i < points.length; i++) {
374 | if(!points[i].isValid()) { continue; }
375 | var marker = this.drawMarker(points[i]);
376 | marker.addTo(this.markerLayer);
377 | this.bounds.extend(marker.getLatLng());
378 | points[i].marker = marker;
379 | };
380 |
381 | this.markerLayer.addTo(this.map);
382 |
383 | if(!this.mapOptions.zoom && !this.mapOptions.center) {
384 | this.map.fitBounds(this.bounds);
385 | }
386 | }
387 | }
388 |
389 | /*
390 |
391 | Did you know you can pass in your own map?
392 | Check out https://gist.github.com/1804938 for some tips on using different tile providers
393 |
394 | */
395 |
396 | Mapsheet.Providers.Leaflet = function(options) {
397 | this.map = options.map;
398 |
399 | var attribution = 'Map data © OpenStreetMap contributors, CC-BY-SA, tiles © MapQuest
';
400 |
401 | var layerDefaults = {
402 | styleId: 998,
403 | attribution: attribution,
404 | type: 'osm'
405 | };
406 |
407 | this.layerOptions = merge_options(layerDefaults, options.layerOptions || {});
408 |
409 | // Only overwrite if there's no tilePath, because the default subdomains is 'abc'
410 | if(!this.layerOptions.tilePath) {
411 | this.layerOptions.tilePath = 'http://otile{s}.mqcdn.com/tiles/1.0.0/{type}/{z}/{x}/{y}.png';
412 | this.layerOptions.subdomains = '1234';
413 | this.layerOptions.type = 'osm';
414 | }
415 | this.markerLayer = options.markerLayer || new L.LayerGroup();
416 | this.mapOptions = options.mapOptions || {};
417 | this.bounds = new L.LatLngBounds();
418 | };
419 |
420 | Mapsheet.Providers.Leaflet.prototype = {
421 | initialize: function(element) {
422 | if(typeof(this.map) === 'undefined') {
423 | this.map = new L.Map('map', this.mapOptions);
424 | this.tileLayer = new L.TileLayer(this.layerOptions['tilePath'], this.layerOptions).addTo(this.map);
425 | }
426 | },
427 |
428 | drawMarker: function(point) {
429 | var marker = L.marker(point.coords())
430 | .bindPopup(point.content())
431 |
432 | if(typeof(point.get('icon url')) !== 'undefined' && point.get('icon url') !== '') {
433 | var options = merge_options( point.markerOptions, { iconUrl: point.get('icon url') } );
434 | var icon = L.icon(options);
435 | marker.setIcon(icon);
436 | } else if(typeof(point.markerOptions['iconUrl']) !== 'undefined') {
437 | var icon = L.icon(point.markerOptions);
438 | marker.setIcon(icon);
439 | }
440 |
441 | if(point.click) {
442 | marker.on('click', function(e) {
443 | point.click.call(this, e, point);
444 | });
445 | }
446 | // var icon = L.icon();
447 | // marker.setIcon(icon);
448 |
449 | return marker;
450 | },
451 |
452 | drawPoints: function(points) {
453 | for(var i = 0; i < points.length; i++) {
454 | if(!points[i].isValid()) { continue; }
455 | var marker = this.drawMarker(points[i]);
456 | marker.addTo(this.markerLayer);
457 | this.bounds.extend(marker.getLatLng());
458 | points[i].marker = marker;
459 | };
460 |
461 | this.markerLayer.addTo(this.map);
462 |
463 | if(!this.mapOptions.zoom && !this.mapOptions.center) {
464 | this.map.fitBounds(this.bounds);
465 | }
466 | }
467 | }
468 |
469 | })(this);
470 |
--------------------------------------------------------------------------------
/demo/tabletop.js:
--------------------------------------------------------------------------------
1 | (function(global) {
2 | "use strict";
3 |
4 | var inNodeJS = false;
5 | if (typeof module !== 'undefined' && module.exports) {
6 | inNodeJS = true;
7 | var request = require('request');
8 | }
9 |
10 | var supportsCORS = false;
11 | var inLegacyIE = false;
12 | try {
13 | var testXHR = new XMLHttpRequest();
14 | if (typeof testXHR.withCredentials !== 'undefined') {
15 | supportsCORS = true;
16 | } else {
17 | if ("XDomainRequest" in window) {
18 | supportsCORS = true;
19 | inLegacyIE = true;
20 | }
21 | }
22 | } catch (e) { }
23 |
24 | // Create a simple indexOf function for support
25 | // of older browsers. Uses native indexOf if
26 | // available. Code similar to underscores.
27 | // By making a separate function, instead of adding
28 | // to the prototype, we will not break bad for loops
29 | // in older browsers
30 | var indexOfProto = Array.prototype.indexOf;
31 | var ttIndexOf = function(array, item) {
32 | var i = 0, l = array.length;
33 |
34 | if (indexOfProto && array.indexOf === indexOfProto) return array.indexOf(item);
35 | for (; i < l; i++) if (array[i] === item) return i;
36 | return -1;
37 | };
38 |
39 | /*
40 | Initialize with Tabletop.init( { key: '0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc' } )
41 | OR!
42 | Initialize with Tabletop.init( { key: 'https://docs.google.com/spreadsheet/pub?hl=en_US&hl=en_US&key=0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc&output=html&widget=true' } )
43 | OR!
44 | Initialize with Tabletop.init('0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc')
45 | */
46 |
47 | var Tabletop = function(options) {
48 | // Make sure Tabletop is being used as a constructor no matter what.
49 | if(!this || !(this instanceof Tabletop)) {
50 | return new Tabletop(options);
51 | }
52 |
53 | if(typeof(options) === 'string') {
54 | options = { key : options };
55 | }
56 |
57 | this.callback = options.callback;
58 | this.wanted = options.wanted || [];
59 | this.key = options.key;
60 | this.simpleSheet = !!options.simpleSheet;
61 | this.parseNumbers = !!options.parseNumbers;
62 | this.wait = !!options.wait;
63 | this.reverse = !!options.reverse;
64 | this.postProcess = options.postProcess;
65 | this.debug = !!options.debug;
66 | this.query = options.query || '';
67 | this.orderby = options.orderby;
68 | this.endpoint = options.endpoint || "https://spreadsheets.google.com";
69 | this.singleton = !!options.singleton;
70 | this.simple_url = !!options.simple_url;
71 | this.callbackContext = options.callbackContext;
72 |
73 | if(typeof(options.proxy) !== 'undefined') {
74 | this.endpoint = options.proxy;
75 | this.simple_url = true;
76 | this.singleton = true;
77 | // Let's only use CORS (straight JSON request) when
78 | // fetching straight from Google
79 | supportsCORS = false
80 | }
81 |
82 | this.parameterize = options.parameterize || false;
83 |
84 | if(this.singleton) {
85 | if(typeof(Tabletop.singleton) !== 'undefined') {
86 | this.log("WARNING! Tabletop singleton already defined");
87 | }
88 | Tabletop.singleton = this;
89 | }
90 |
91 | /* Be friendly about what you accept */
92 | if(/key=/.test(this.key)) {
93 | this.log("You passed a key as a URL! Attempting to parse.");
94 | this.key = this.key.match("key=(.*?)&")[1];
95 | }
96 |
97 | if(!this.key) {
98 | this.log("You need to pass Tabletop a key!");
99 | return;
100 | }
101 |
102 | this.log("Initializing with key " + this.key);
103 |
104 | this.models = {};
105 | this.model_names = [];
106 |
107 | this.base_json_path = "/feeds/worksheets/" + this.key + "/public/basic?alt=";
108 |
109 | if (inNodeJS || supportsCORS) {
110 | this.base_json_path += 'json';
111 | } else {
112 | this.base_json_path += 'json-in-script';
113 | }
114 |
115 | if(!this.wait) {
116 | this.fetch();
117 | }
118 | };
119 |
120 | // A global storage for callbacks.
121 | Tabletop.callbacks = {};
122 |
123 | // Backwards compatibility.
124 | Tabletop.init = function(options) {
125 | return new Tabletop(options);
126 | };
127 |
128 | Tabletop.sheets = function() {
129 | this.log("Times have changed! You'll want to use var tabletop = Tabletop.init(...); tabletop.sheets(...); instead of Tabletop.sheets(...)");
130 | };
131 |
132 | Tabletop.prototype = {
133 |
134 | fetch: function(callback) {
135 | if(typeof(callback) !== "undefined") {
136 | this.callback = callback;
137 | }
138 | this.requestData(this.base_json_path, this.loadSheets);
139 | },
140 |
141 | /*
142 | This will call the environment appropriate request method.
143 |
144 | In browser it will use JSON-P, in node it will use request()
145 | */
146 | requestData: function(path, callback) {
147 | if (inNodeJS) {
148 | this.serverSideFetch(path, callback);
149 | } else {
150 | //CORS only works in IE8/9 across the same protocol
151 | //You must have your server on HTTPS to talk to Google, or it'll fall back on injection
152 | var protocol = this.endpoint.split("//").shift() || "http";
153 | if (supportsCORS && (!inLegacyIE || protocol === location.protocol)) {
154 | this.xhrFetch(path, callback);
155 | } else {
156 | this.injectScript(path, callback);
157 | }
158 | }
159 | },
160 |
161 | /*
162 | Use Cross-Origin XMLHttpRequest to get the data in browsers that support it.
163 | */
164 | xhrFetch: function(path, callback) {
165 | //support IE8's separate cross-domain object
166 | var xhr = inLegacyIE ? new XDomainRequest() : new XMLHttpRequest();
167 | xhr.open("GET", this.endpoint + path);
168 | var self = this;
169 | xhr.onload = function() {
170 | try {
171 | var json = JSON.parse(xhr.responseText);
172 | } catch (e) {
173 | console.error(e);
174 | }
175 | callback.call(self, json);
176 | };
177 | xhr.send();
178 | },
179 |
180 | /*
181 | Insert the URL into the page as a script tag. Once it's loaded the spreadsheet data
182 | it triggers the callback. This helps you avoid cross-domain errors
183 | http://code.google.com/apis/gdata/samples/spreadsheet_sample.html
184 |
185 | Let's be plain-Jane and not use jQuery or anything.
186 | */
187 | injectScript: function(path, callback) {
188 | var script = document.createElement('script');
189 | var callbackName;
190 |
191 | if(this.singleton) {
192 | if(callback === this.loadSheets) {
193 | callbackName = 'Tabletop.singleton.loadSheets';
194 | } else if (callback === this.loadSheet) {
195 | callbackName = 'Tabletop.singleton.loadSheet';
196 | }
197 | } else {
198 | var self = this;
199 | callbackName = 'tt' + (+new Date()) + (Math.floor(Math.random()*100000));
200 | // Create a temp callback which will get removed once it has executed,
201 | // this allows multiple instances of Tabletop to coexist.
202 | Tabletop.callbacks[ callbackName ] = function () {
203 | var args = Array.prototype.slice.call( arguments, 0 );
204 | callback.apply(self, args);
205 | script.parentNode.removeChild(script);
206 | delete Tabletop.callbacks[callbackName];
207 | };
208 | callbackName = 'Tabletop.callbacks.' + callbackName;
209 | }
210 |
211 | var url = path + "&callback=" + callbackName;
212 |
213 | if(this.simple_url) {
214 | // We've gone down a rabbit hole of passing injectScript the path, so let's
215 | // just pull the sheet_id out of the path like the least efficient worker bees
216 | if(path.indexOf("/list/") !== -1) {
217 | script.src = this.endpoint + "/" + this.key + "-" + path.split("/")[4];
218 | } else {
219 | script.src = this.endpoint + "/" + this.key;
220 | }
221 | } else {
222 | script.src = this.endpoint + url;
223 | }
224 |
225 | if (this.parameterize) {
226 | script.src = this.parameterize + encodeURIComponent(script.src);
227 | }
228 |
229 | document.getElementsByTagName('script')[0].parentNode.appendChild(script);
230 | },
231 |
232 | /*
233 | This will only run if tabletop is being run in node.js
234 | */
235 | serverSideFetch: function(path, callback) {
236 | var self = this
237 | request({url: this.endpoint + path, json: true}, function(err, resp, body) {
238 | if (err) {
239 | return console.error(err);
240 | }
241 | callback.call(self, body);
242 | });
243 | },
244 |
245 | /*
246 | Is this a sheet you want to pull?
247 | If { wanted: ["Sheet1"] } has been specified, only Sheet1 is imported
248 | Pulls all sheets if none are specified
249 | */
250 | isWanted: function(sheetName) {
251 | if(this.wanted.length === 0) {
252 | return true;
253 | } else {
254 | return (ttIndexOf(this.wanted, sheetName) !== -1);
255 | }
256 | },
257 |
258 | /*
259 | What gets send to the callback
260 | if simpleSheet === true, then don't return an array of Tabletop.this.models,
261 | only return the first one's elements
262 | */
263 | data: function() {
264 | // If the instance is being queried before the data's been fetched
265 | // then return undefined.
266 | if(this.model_names.length === 0) {
267 | return undefined;
268 | }
269 | if(this.simpleSheet) {
270 | if(this.model_names.length > 1 && this.debug) {
271 | this.log("WARNING You have more than one sheet but are using simple sheet mode! Don't blame me when something goes wrong.");
272 | }
273 | return this.models[ this.model_names[0] ].all();
274 | } else {
275 | return this.models;
276 | }
277 | },
278 |
279 | /*
280 | Add another sheet to the wanted list
281 | */
282 | addWanted: function(sheet) {
283 | if(ttIndexOf(this.wanted, sheet) === -1) {
284 | this.wanted.push(sheet);
285 | }
286 | },
287 |
288 | /*
289 | Load all worksheets of the spreadsheet, turning each into a Tabletop Model.
290 | Need to use injectScript because the worksheet view that you're working from
291 | doesn't actually include the data. The list-based feed (/feeds/list/key..) does, though.
292 | Calls back to loadSheet in order to get the real work done.
293 |
294 | Used as a callback for the worksheet-based JSON
295 | */
296 | loadSheets: function(data) {
297 | var i, ilen;
298 | var toLoad = [];
299 | this.foundSheetNames = [];
300 |
301 | for(i = 0, ilen = data.feed.entry.length; i < ilen ; i++) {
302 | this.foundSheetNames.push(data.feed.entry[i].title.$t);
303 | // Only pull in desired sheets to reduce loading
304 | if( this.isWanted(data.feed.entry[i].content.$t) ) {
305 | var sheet_id = data.feed.entry[i].link[3].href.substr( data.feed.entry[i].link[3].href.length - 3, 3);
306 | var json_path = "/feeds/list/" + this.key + "/" + sheet_id + "/public/values?sq=" + this.query + '&alt='
307 | if (inNodeJS || supportsCORS) {
308 | json_path += 'json';
309 | } else {
310 | json_path += 'json-in-script';
311 | }
312 | if(this.orderby) {
313 | json_path += "&orderby=column:" + this.orderby.toLowerCase();
314 | }
315 | if(this.reverse) {
316 | json_path += "&reverse=true";
317 | }
318 | toLoad.push(json_path);
319 | }
320 | }
321 |
322 | this.sheetsToLoad = toLoad.length;
323 | for(i = 0, ilen = toLoad.length; i < ilen; i++) {
324 | this.requestData(toLoad[i], this.loadSheet);
325 | }
326 | },
327 |
328 | /*
329 | Access layer for the this.models
330 | .sheets() gets you all of the sheets
331 | .sheets('Sheet1') gets you the sheet named Sheet1
332 | */
333 | sheets: function(sheetName) {
334 | if(typeof sheetName === "undefined") {
335 | return this.models;
336 | } else {
337 | if(typeof(this.models[ sheetName ]) === "undefined") {
338 | // alert( "Can't find " + sheetName );
339 | return;
340 | } else {
341 | return this.models[ sheetName ];
342 | }
343 | }
344 | },
345 |
346 | /*
347 | Parse a single list-based worksheet, turning it into a Tabletop Model
348 |
349 | Used as a callback for the list-based JSON
350 | */
351 | loadSheet: function(data) {
352 | var model = new Tabletop.Model( { data: data,
353 | parseNumbers: this.parseNumbers,
354 | postProcess: this.postProcess,
355 | tabletop: this } );
356 | this.models[ model.name ] = model;
357 | if(ttIndexOf(this.model_names, model.name) === -1) {
358 | this.model_names.push(model.name);
359 | }
360 | this.sheetsToLoad--;
361 | if(this.sheetsToLoad === 0)
362 | this.doCallback();
363 | },
364 |
365 | /*
366 | Execute the callback upon loading! Rely on this.data() because you might
367 | only request certain pieces of data (i.e. simpleSheet mode)
368 | Tests this.sheetsToLoad just in case a race condition happens to show up
369 | */
370 | doCallback: function() {
371 | if(this.sheetsToLoad === 0) {
372 | this.callback.apply(this.callbackContext || this, [this.data(), this]);
373 | }
374 | },
375 |
376 | log: function(msg) {
377 | if(this.debug) {
378 | if(typeof console !== "undefined" && typeof console.log !== "undefined") {
379 | Function.prototype.apply.apply(console.log, [console, arguments]);
380 | }
381 | }
382 | }
383 |
384 | };
385 |
386 | /*
387 | Tabletop.Model stores the attribute names and parses the worksheet data
388 | to turn it into something worthwhile
389 |
390 | Options should be in the format { data: XXX }, with XXX being the list-based worksheet
391 | */
392 | Tabletop.Model = function(options) {
393 | var i, j, ilen, jlen;
394 | this.column_names = [];
395 | this.name = options.data.feed.title.$t;
396 | this.elements = [];
397 | this.raw = options.data; // A copy of the sheet's raw data, for accessing minutiae
398 |
399 | if(typeof(options.data.feed.entry) === 'undefined') {
400 | options.tabletop.log("Missing data for " + this.name + ", make sure you didn't forget column headers");
401 | this.elements = [];
402 | return;
403 | }
404 |
405 | for(var key in options.data.feed.entry[0]){
406 | if(/^gsx/.test(key))
407 | this.column_names.push( key.replace("gsx$","") );
408 | }
409 |
410 | for(i = 0, ilen = options.data.feed.entry.length ; i < ilen; i++) {
411 | var source = options.data.feed.entry[i];
412 | var element = {};
413 | for(var j = 0, jlen = this.column_names.length; j < jlen ; j++) {
414 | var cell = source[ "gsx$" + this.column_names[j] ];
415 | if (typeof(cell) !== 'undefined') {
416 | if(options.parseNumbers && cell.$t !== '' && !isNaN(cell.$t))
417 | element[ this.column_names[j] ] = +cell.$t;
418 | else
419 | element[ this.column_names[j] ] = cell.$t;
420 | } else {
421 | element[ this.column_names[j] ] = '';
422 | }
423 | }
424 | if(element.rowNumber === undefined)
425 | element.rowNumber = i + 1;
426 | if( options.postProcess )
427 | options.postProcess(element);
428 | this.elements.push(element);
429 | }
430 |
431 | };
432 |
433 | Tabletop.Model.prototype = {
434 | /*
435 | Returns all of the elements (rows) of the worksheet as objects
436 | */
437 | all: function() {
438 | return this.elements;
439 | },
440 |
441 | /*
442 | Return the elements as an array of arrays, instead of an array of objects
443 | */
444 | toArray: function() {
445 | var array = [],
446 | i, j, ilen, jlen;
447 | for(i = 0, ilen = this.elements.length; i < ilen; i++) {
448 | var row = [];
449 | for(j = 0, jlen = this.column_names.length; j < jlen ; j++) {
450 | row.push( this.elements[i][ this.column_names[j] ] );
451 | }
452 | array.push(row);
453 | }
454 | return array;
455 | }
456 | };
457 |
458 | if(inNodeJS) {
459 | module.exports = Tabletop;
460 | } else {
461 | global.Tabletop = Tabletop;
462 | }
463 |
464 | })(this);
465 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "simplegdocserver",
3 | "version": "0.0.1",
4 | "author": "sheetjs",
5 | "description": "Simple REST Server that fakes Google Docs interface for reading data",
6 | "keywords": [ "xls", "xlsx", "xlsb", "xlsm", "excel", "spreadsheet", "google-docs", "gdata" ],
7 | "bin": "./server.js",
8 | "license": "Apache-2.0",
9 | "repository": { "type":"git", "url":"https://github.com/sheetjs/sgds" },
10 | "dependencies": {
11 | "j": "~0.2.6",
12 | "browserver-router": "~0.1.1",
13 | "cors": "~2.1.1",
14 | "connect": "~2.12.0"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | /* server.js (C) 2014 SheetJS -- http://sheetjs.com */
3 | /* vim: set ts=2: */
4 | /* setup: npm install j connect cors browserver-router
5 | start: node server.js [port [base_directory]]
6 | will serve xls{,x,b,m} files from the base_directory using standard connect
7 |
8 | use with tabletop:
9 | Tabletop.init( {
10 | endpoint:"http://localhost:7263", // <-- adjust accordingly
11 | key: "myfile.xls", // <-- the actual filename
12 | ...
13 | });
14 | */
15 |
16 | var http = require('http'),
17 | J = require('j'),
18 | connect = require('connect'),
19 | cors = require('cors'),
20 | Router = require('browserver-router'),
21 | path = require('path');
22 |
23 | var port = Number(process.argv[2]||0) || process.env.PORT || 7263;
24 | var cwd = process.argv[3] || process.env.BASE_DIR || process.cwd();
25 |
26 | var diropts = {
27 | icons: true,
28 | filter: function(name) {
29 | return name.match(/\.xls[xbm]?$/);
30 | }
31 | }
32 |
33 | var tt_entries = function(req, res) {
34 | var args = req.params[0].split("~");
35 | var w = J.readFile(path.join(cwd,args[0]));
36 | var out = JSON.stringify({
37 | feed: {
38 | entry: w[1].SheetNames.map(function(ss,i) { return {
39 | title:{"$t":ss},
40 | content:{"$t":""},
41 | link:[
42 | {},
43 | {},
44 | {},
45 | {href:"000"+String(i)}
46 | ]
47 | };})
48 | }
49 | })
50 | if(req.query && req.query.alt == 'json-in-script') {
51 | out = req.query.callback + '(JSON.parse(' + JSON.stringify(out) + '));'
52 | }
53 | res.end(out);
54 | };
55 |
56 | var tt_data = function(req, res) {
57 | var name = req.params[0], sidx = req.params[1];
58 | var w = J.readFile(path.join(cwd,name));
59 | var s = w[1].SheetNames[Number(sidx)];
60 | var o = J.utils.to_json(w, name, s);
61 | if(s) o = o[s];
62 | o.forEach(function(r) {
63 | Object.keys(r).forEach(function(k) {
64 | r["gsx$"+k] = {"$t":r[k]}; delete r[k];
65 | });
66 | });
67 | var out = JSON.stringify({feed:{entry:o, title:{"$t":s}}});
68 | if(req.query && req.query.alt == 'json-in-script') {
69 | out = req.query.callback + '(JSON.parse(' + JSON.stringify(out) + '));'
70 | }
71 | res.writeHead(200, {'Content-Type':'application/javascript'});
72 | res.end(out);
73 | };
74 |
75 | var router = Router({
76 | '/feeds/worksheets/:file/public/basic': tt_entries,
77 | '/feeds/list/:file/:idx/public/values': tt_data,
78 | });
79 |
80 | var app = connect()
81 | .use(cors())
82 | .use(connect.logger())
83 | .use(connect.bodyParser())
84 | .use(connect.query())
85 | .use(router)
86 | .use(connect.directory(cwd, diropts));
87 |
88 | http.createServer(app).listen(port);
89 |
--------------------------------------------------------------------------------