/** * 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. */ package com.etherpad; object Easysync2Support { def numToString(d: Int): String = java.lang.Integer.toString(d.toInt, 36); // lowercase def stringToNum(s: String): Int = java.lang.Integer.parseInt(s, 36); def opAssembler() = new OpAssembler(); class OpAssembler() { val buf = new StringBuilder(1000); def append(op: Op) { append(op.opcode, op.chars, op.lines, op.attribs); } def append(opcode: String, chars: Int, lines: Int, attribs: String) { buf.append(attribs); if (lines > 0) { buf.append('|'); buf.append(numToString(lines)); } buf.append(opcode); buf.append(numToString(chars)); } override def toString(): String = buf.toString; def clear() { buf.clear; } } def isAlphanum(c: Char) = (c >= '0' && c <= '9' || c >= 'a' && c <= 'z'); case object OpParseError extends Error; def nextOpInString(str: String, startIndex: Int): Object = { var i = startIndex; try { def lookingAt(c: Char) = (i < str.length && str.charAt(i) == c); def lookingAtAlphanum() = (i < str.length && isAlphanum(str.charAt(i))); def atEnd() = (i >= str.length); def readAlphanum(): Int = { if (! lookingAtAlphanum()) { throw OpParseError; } val start = i; while (lookingAtAlphanum()) { i += 1; } val end = i; stringToNum(str.substring(start, end)); } while (lookingAt('*')) { i += 1; if (! lookingAtAlphanum()) { throw OpParseError; } while (lookingAtAlphanum()) { i += 1; } } val attribsEnd = i; var lines_ = 0; if (lookingAt('|')) { i += 1; lines_ = readAlphanum(); } if (lookingAt('?')) { return new { val opcode = "?"; } } if (! (lookingAt('+') || lookingAt('-') || lookingAt('='))) { throw OpParseError; } val opcode_ = str.substring(i, i+1); i += 1; val chars_ = readAlphanum(); return new Op(opcode_, chars_, lines_, str.substring(startIndex, attribsEnd)) { val lastIndex = i; }; } catch { case OpParseError => null } } case class Op(var opcode: String, var chars: Int, var lines: Int, var attribs: String); def newOp() = Op("", 0, 0, ""); def clearOp(op: Op) { op.opcode = ""; op.chars = 0; op.lines = 0; op.attribs = ""; } // ported from easysync2.js class MergingOpAssembler { val assem = opAssembler(); var bufOp = newOp(); var bufOpAdditionalCharsAfterNewline = 0; def flush(isEndDocument: Boolean) { if (bufOp.opcode.length > 0) { if (isEndDocument && bufOp.opcode == "=" && bufOp.attribs.length == 0) { // final merged keep, leave it implicit } else { assem.append(bufOp); if (bufOpAdditionalCharsAfterNewline > 0) { bufOp.chars = bufOpAdditionalCharsAfterNewline; bufOp.lines = 0; assem.append(bufOp); bufOpAdditionalCharsAfterNewline = 0; } } bufOp.opcode = ""; } } def append(opcode: String, chars: Int, lines: Int, attribs: String) { if (chars > 0) { if (bufOp.opcode == opcode && bufOp.attribs == attribs) { if (lines > 0) { // bufOp and additional chars are all mergeable into a multi-line op bufOp.chars += bufOpAdditionalCharsAfterNewline + chars; bufOp.lines += lines; bufOpAdditionalCharsAfterNewline = 0; } else if (bufOp.lines == 0) { // both bufOp and op are in-line bufOp.chars += chars; } else { // append in-line text to multi-line bufOp bufOpAdditionalCharsAfterNewline += chars; } } else { flush(false); bufOp = Op(opcode, chars, lines, attribs); } } } def endDocument() { flush(true); } override def toString() = { flush(false); assem.toString(); } def clear() { assem.clear(); clearOp(bufOp); } } def mergingOpAssembler() = new MergingOpAssembler(); }