text.ts 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. /**
  2. * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * Adapted from CIFTools.js (https://github.com/dsehnal/CIFTools.js)
  5. *
  6. * @author David Sehnal <david.sehnal@gmail.com>
  7. */
  8. import { Column } from '../../../../mol-data/db'
  9. import StringBuilder from '../../../../mol-util/string-builder'
  10. import { Category, Field, Encoder } from '../encoder'
  11. import Writer from '../../writer'
  12. import { getFieldDigitCount, getIncludedFields, getCategoryInstanceData, CategoryInstanceData } from './util';
  13. export default class TextEncoder implements Encoder<string> {
  14. private builder = StringBuilder.create();
  15. private encoded = false;
  16. private dataBlockCreated = false;
  17. private filter: Category.Filter = Category.DefaultFilter;
  18. private formatter: Category.Formatter = Category.DefaultFormatter;
  19. binaryEncodingProvider = void 0;
  20. setFilter(filter?: Category.Filter) {
  21. this.filter = filter || Category.DefaultFilter;
  22. }
  23. isCategoryIncluded(name: string) {
  24. return this.filter.includeCategory(name);
  25. }
  26. setFormatter(formatter?: Category.Formatter) {
  27. this.formatter = formatter || Category.DefaultFormatter;
  28. }
  29. startDataBlock(header: string) {
  30. this.dataBlockCreated = true;
  31. StringBuilder.write(this.builder, `data_${(header || '').replace(/[ \n\t]/g, '').toUpperCase()}\n#\n`);
  32. }
  33. writeCategory<Ctx>(category: Category<Ctx>, context?: Ctx, options?: Encoder.WriteCategoryOptions) {
  34. if (this.encoded) {
  35. throw new Error('The writer contents have already been encoded, no more writing.');
  36. }
  37. if (!this.dataBlockCreated) {
  38. throw new Error('No data block created.');
  39. }
  40. if (!options?.ignoreFilter && !this.filter.includeCategory(category.name)) return;
  41. const { instance, rowCount, source } = getCategoryInstanceData(category, context);
  42. if (!rowCount) return;
  43. if (rowCount === 1) {
  44. writeCifSingleRecord(category, instance, source, this.builder, this.filter, this.formatter);
  45. } else {
  46. writeCifLoop(category, instance, source, this.builder, this.filter, this.formatter);
  47. }
  48. }
  49. encode() {
  50. this.encoded = true;
  51. }
  52. writeTo(stream: Writer) {
  53. const chunks = StringBuilder.getChunks(this.builder);
  54. for (let i = 0, _i = chunks.length; i < _i; i++) {
  55. stream.writeString(chunks[i]);
  56. }
  57. }
  58. getSize() {
  59. return StringBuilder.getSize(this.builder);
  60. }
  61. getData() {
  62. return StringBuilder.getString(this.builder);
  63. }
  64. }
  65. function writeValue(builder: StringBuilder, data: any, key: any, f: Field<any, any>, floatPrecision: number, index: number): boolean {
  66. const kind = f.valueKind;
  67. const p = kind ? kind(key, data) : Column.ValueKind.Present;
  68. if (p !== Column.ValueKind.Present) {
  69. if (p === Column.ValueKind.NotPresent) writeNotPresent(builder);
  70. else writeUnknown(builder);
  71. } else {
  72. const val = f.value(key, data, index);
  73. const t = f.type;
  74. if (t === Field.Type.Str) {
  75. if (isMultiline(val as string)) {
  76. writeMultiline(builder, val as string);
  77. return true;
  78. } else {
  79. return writeChecked(builder, val as string);
  80. }
  81. } else if (t === Field.Type.Int) {
  82. writeInteger(builder, val as number);
  83. } else {
  84. writeFloat(builder, val as number, floatPrecision);
  85. }
  86. }
  87. return false;
  88. }
  89. function getFloatPrecisions(categoryName: string, fields: Field[], formatter: Category.Formatter) {
  90. const ret: number[] = [];
  91. for (const f of fields) {
  92. const format = formatter.getFormat(categoryName, f.name);
  93. if (format && typeof format.digitCount !== 'undefined') ret[ret.length] = f.type === Field.Type.Float ? Math.pow(10, Math.max(0, Math.min(format.digitCount, 15))) : 0;
  94. else ret[ret.length] = f.type === Field.Type.Float ? Math.pow(10, getFieldDigitCount(f)) : 0;
  95. }
  96. return ret;
  97. }
  98. function writeCifSingleRecord(category: Category, instance: Category.Instance, source: CategoryInstanceData['source'], builder: StringBuilder, filter: Category.Filter, formatter: Category.Formatter) {
  99. const fields = getIncludedFields(instance);
  100. const src = source[0];
  101. const data = src.data;
  102. let width = fields.reduce((w, f) => filter.includeField(category.name, f.name) ? Math.max(w, f.name.length) : 0, 0);
  103. // this means no field from this category is included.
  104. if (width === 0) return;
  105. width += category.name.length + 6;
  106. const it = src.keys();
  107. const key = it.move();
  108. const precisions = getFloatPrecisions(category.name, instance.fields, formatter);
  109. for (let _f = 0; _f < fields.length; _f++) {
  110. const f = fields[_f];
  111. if (!filter.includeField(category.name, f.name)) continue;
  112. StringBuilder.writePadRight(builder, `_${category.name}.${f.name}`, width);
  113. const multiline = writeValue(builder, data, key, f, precisions[_f], 0);
  114. if (!multiline) StringBuilder.newline(builder);
  115. }
  116. StringBuilder.write(builder, '#\n');
  117. }
  118. function writeCifLoop(category: Category, instance: Category.Instance, source: CategoryInstanceData['source'], builder: StringBuilder, filter: Category.Filter, formatter: Category.Formatter) {
  119. const fieldSource = getIncludedFields(instance);
  120. const fields = filter === Category.DefaultFilter ? fieldSource : fieldSource.filter(f => filter.includeField(category.name, f.name));
  121. const fieldCount = fields.length;
  122. if (fieldCount === 0) return;
  123. const precisions = getFloatPrecisions(category.name, fields, formatter);
  124. writeLine(builder, 'loop_');
  125. for (let i = 0; i < fieldCount; i++) {
  126. writeLine(builder, `_${category.name}.${fields[i].name}`);
  127. }
  128. let index = 0;
  129. for (let _c = 0; _c < source.length; _c++) {
  130. const src = source[_c];
  131. const data = src.data;
  132. if (src.rowCount === 0) continue;
  133. const it = src.keys();
  134. while (it.hasNext) {
  135. const key = it.move();
  136. let multiline = false;
  137. for (let _f = 0; _f < fieldCount; _f++) {
  138. multiline = writeValue(builder, data, key, fields[_f], precisions[_f], index);
  139. }
  140. if (!multiline) StringBuilder.newline(builder);
  141. index++;
  142. }
  143. }
  144. StringBuilder.write(builder, '#\n');
  145. }
  146. function isMultiline(value: string) {
  147. return typeof value === 'string' && value.indexOf('\n') >= 0;
  148. }
  149. function writeLine(builder: StringBuilder, val: string) {
  150. StringBuilder.write(builder, val);
  151. StringBuilder.newline(builder);
  152. }
  153. function writeInteger(builder: StringBuilder, val: number) {
  154. StringBuilder.writeInteger(builder, val);
  155. StringBuilder.whitespace1(builder);
  156. }
  157. function writeFloat(builder: StringBuilder, val: number, precisionMultiplier: number) {
  158. StringBuilder.writeFloat(builder, val, precisionMultiplier);
  159. StringBuilder.whitespace1(builder);
  160. }
  161. function writeNotPresent(builder: StringBuilder) {
  162. StringBuilder.writeSafe(builder, '. ');
  163. }
  164. function writeUnknown(builder: StringBuilder) {
  165. StringBuilder.writeSafe(builder, '? ');
  166. }
  167. function writeChecked(builder: StringBuilder, val: string) {
  168. if (!val) {
  169. StringBuilder.writeSafe(builder, '. ');
  170. return false;
  171. }
  172. let escape = val.charCodeAt(0) === 95 /* _ */, escapeCharStart = '\'', escapeCharEnd = '\' ';
  173. let hasWhitespace = false;
  174. let hasSingle = false;
  175. let hasDouble = false;
  176. for (let i = 0, _l = val.length - 1; i < _l; i++) {
  177. const c = val.charCodeAt(i);
  178. switch (c) {
  179. case 9: hasWhitespace = true; break; // \t
  180. case 10: // \n
  181. writeMultiline(builder, val);
  182. return true;
  183. case 32: hasWhitespace = true; break; // ' '
  184. case 34: // "
  185. if (hasSingle) {
  186. writeMultiline(builder, val);
  187. return true;
  188. }
  189. hasDouble = true;
  190. escape = true;
  191. escapeCharStart = '\'';
  192. escapeCharEnd = '\' ';
  193. break;
  194. case 39: // '
  195. if (hasDouble) {
  196. writeMultiline(builder, val);
  197. return true;
  198. }
  199. escape = true;
  200. hasSingle = true;
  201. escapeCharStart = '"';
  202. escapeCharEnd = '" ';
  203. break;
  204. }
  205. }
  206. const fst = val.charCodeAt(0);
  207. if (!escape && (fst === 35 /* # */|| fst === 36 /* $ */ || fst === 59 /* ; */ || fst === 91 /* [ */ || fst === 93 /* ] */ || hasWhitespace)) {
  208. escapeCharStart = '\'';
  209. escapeCharEnd = '\' ';
  210. escape = true;
  211. }
  212. if (escape) {
  213. StringBuilder.writeSafe(builder, escapeCharStart);
  214. StringBuilder.writeSafe(builder, val);
  215. StringBuilder.writeSafe(builder, escapeCharEnd);
  216. } else {
  217. StringBuilder.writeSafe(builder, val);
  218. StringBuilder.writeSafe(builder, ' ');
  219. }
  220. return false;
  221. }
  222. function writeMultiline(builder: StringBuilder, val: string) {
  223. StringBuilder.writeSafe(builder, '\n;' + val);
  224. StringBuilder.writeSafe(builder, '\n;\n');
  225. }