/*
THIS IS A GENERATED/BUNDLED FILE BY ROLLUP
if you want to view the source visit the plugin's github repository
*/

'use strict';

var obsidian = require('obsidian');

/*! *****************************************************************************
Copyright (c) Microsoft Corporation.

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */

function __awaiter(thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
}

var codemirror = CodeMirror;

function createCommonjsModule(fn) {
  var module = { exports: {} };
	return fn(module, module.exports), module.exports;
}

createCommonjsModule(function (module, exports) {
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE

(function(mod) {
  mod(codemirror);
})(function(CodeMirror) {

CodeMirror.defineMode("cook", function() {
  return {
    token: function(stream, state) {
      var sol = stream.sol() || state.afterSection;
      var eol = stream.eol();

      state.afterSection = false;

      if (sol) {
        if (state.nextMultiline) {
          state.inMultiline = true;
          state.nextMultiline = false;
        } else {
          state.position = null;
        }
      }

      if (eol && ! state.nextMultiline) {
        state.inMultiline = false;
        state.position = null;
      }

      if (sol) {
        while(stream.eatSpace()) {}
      }

      var ch = stream.next();

      
      if (sol && ch === ">") {
        if (stream.eat(">")) {
          state.position = "metadata-key";
          return "metadata"
        }
      }
      if(state.position === "metadata");
      else if(state.position === "metadata-key") {
        if(ch === ':') state.position = "metadata";
      }
      else {
        if (ch === "-") {
          if (stream.eat("-")) {
            stream.skipToEnd();
            return "comment";
          }
        }

        if (stream.match(/\[-.+?-\]/))
          return "comment";

        if(stream.match(/^@([^@#~]+?(?={))/))
          return "ingredient";
        else if(stream.match(/^@(.+?\b)/))
          return "ingredient";
        
        if(stream.match(/^#([^@#~]+?(?={))/))
          return "cookware";
        else if(stream.match(/^#(.+?\b)/))
          return "cookware";

        if(ch === '~'){
          state.position = "timer";
          return "formatting";
        }
        if(ch === '{'){
          if(state.position != "timer") state.position = "measurement";
          return "formatting";
        }
        if(ch === '}'){
          state.position = null;
          return "formatting";
        }
        if(ch === '%' && (state.position === "measurement" || state.position === "timer")){
          state.position = "unit";
          return "formatting";
        }
      }

      return state.position;
    },

    startState: function() {
      return {
        formatting : false,
        nextMultiline : false,  // Is the next line multiline value
        inMultiline : false,    // Is the current line a multiline value
        afterSection : false    // Did we just open a section
      };
    }

  };
});

CodeMirror.defineMIME("text/x-cook", "cook");
CodeMirror.defineMIME("text/x-cooklang", "cook");

});
});

// utility class for parsing cooklang files
class CookLang {
    static parse(source) {
        const recipe = new Recipe();
        source.split('\n').forEach(line => {
            let match;
            // clear comments
            line = line.replace(/(--.*)|(\[-.+?-\])/, '');
            // skip blank lines
            if (line.trim().length === 0)
                return;
            // metadata lines
            else if (match = Metadata.regex.exec(line)) {
                recipe.metadata.push(new Metadata(match[0]));
            }
            // method lines
            else {
                // ingredients on a line
                while (match = Ingredient.regex.exec(line)) {
                    const ingredient = new Ingredient(match[0]);
                    recipe.ingredients.push(ingredient);
                    line = line.replace(match[0], ingredient.methodOutput());
                }
                // cookware on a line
                while (match = Cookware.regex.exec(line)) {
                    const c = new Cookware(match[0]);
                    recipe.cookware.push(c);
                    line = line.replace(match[0], c.methodOutput());
                }
                // timers on a line
                while (match = Timer.regex.exec(line)) {
                    const t = new Timer(match[0]);
                    recipe.timers.push(t);
                    line = line.replace(match[0], t.methodOutput());
                }
                // add in the method line
                recipe.method.push(line.trim());
            }
        });
        return recipe;
    }
}
// a class representing a recipe
class Recipe {
    constructor() {
        this.metadata = [];
        this.ingredients = [];
        this.cookware = [];
        this.timers = [];
        this.method = [];
        this.methodImages = new Map();
    }
    calculateTotalTime() {
        let time = 0;
        this.timers.forEach(timer => {
            let amount = 0;
            if (parseFloat(timer.amount) + '' == timer.amount)
                amount = parseFloat(timer.amount);
            else if (timer.amount.contains('/')) {
                const split = timer.amount.split('/');
                if (split.length == 2) {
                    const num = parseFloat(split[0]);
                    const den = parseFloat(split[1]);
                    if (num && den) {
                        amount = num / den;
                    }
                }
            }
            if (amount > 0) {
                if (timer.unit.toLowerCase().startsWith('s')) {
                    time += amount;
                }
                else if (timer.unit.toLowerCase().startsWith('m')) {
                    time += amount * 60;
                }
                else if (timer.unit.toLowerCase().startsWith('h')) {
                    time += amount * 60 * 60;
                }
            }
        });
        return time;
    }
}
// a class representing an ingredient
class Ingredient {
    constructor(s) {
        var _a;
        this.originalString = null;
        this.name = null;
        this.amount = null;
        this.unit = null;
        this.methodOutput = () => {
            let s = `<span class='ingredient'>`;
            if (this.amount !== null) {
                s += `<span class='amount'>${this.amount} </span>`;
            }
            if (this.unit !== null) {
                s += `<span class='unit'>${this.unit} </span>`;
            }
            s += `${this.name}</span>`;
            return s;
        };
        this.listOutput = () => {
            let s = ``;
            if (this.amount !== null) {
                s += `<span class='amount'>${this.amount}</span> `;
            }
            if (this.unit !== null) {
                s += `<span class='unit'>${this.unit}</span> `;
            }
            s += this.name;
            return s;
        };
        this.originalString = s;
        const match = Ingredient.regex.exec(s);
        this.name = match[1] || match[3];
        const attrs = (_a = match[2]) === null || _a === void 0 ? void 0 : _a.split('%');
        this.amount = attrs && attrs.length > 0 ? attrs[0] : null;
        this.unit = attrs && attrs.length > 1 ? attrs[1] : null;
    }
}
// starts with an @, ends at a word boundary or {}
// (also capture what's inside the {})
Ingredient.regex = /@(?:([^@#~]+?)(?:{(.*?)}|{}))|@(.+?\b)/;
// a class representing an item of cookware
class Cookware {
    constructor(s) {
        this.originalString = null;
        this.name = null;
        this.methodOutput = () => {
            return `<span class='cookware'>${this.name}</span>`;
        };
        this.listOutput = () => {
            return this.name;
        };
        this.originalString = s;
        const match = Cookware.regex.exec(s);
        this.name = match[1] || match[2];
    }
}
// starts with a #, ends at a word boundary or {}
Cookware.regex = /#(?:([^@#~]+?)(?:{}))|#(.+?\b)/;
// a class representing a timer
class Timer {
    constructor(s) {
        this.originalString = null;
        this.amount = null;
        this.unit = null;
        this.methodOutput = () => {
            return `<span class='time'><span class='time-amount'>${this.amount}</span> <span class='time-unit'>${this.unit}</span></span>`;
        };
        this.listOutput = () => {
            return `<span class='time-amount'>${this.amount}</span> <span class='time-unit'>${this.unit}</span>`;
        };
        const match = Timer.regex.exec(s);
        this.amount = match[1];
        this.unit = match[2];
    }
}
// contained within ~{}
Timer.regex = /~{([0-9]+)%(.+?)}/;
// a class representing metadata item
class Metadata {
    constructor(s) {
        this.originalString = null;
        this.key = null;
        this.value = null;
        this.methodOutput = () => {
            return `<span class='metadata metadata-key'>${this.key}</span> <span class='metadata metadata-value'>${this.value}</span>`;
        };
        this.listOutput = () => {
            return `<span class='metadata-key'>${this.key}</span> <span class='metadata-value'>${this.value}</span>`;
        };
        const match = Metadata.regex.exec(s);
        this.key = match[1].trim();
        this.value = match[2].trim();
    }
}
// starts with >>
Metadata.regex = />>\s*(.*?):\s*(.*)/;

// This is the custom view
class CookView extends obsidian.TextFileView {
    constructor(leaf, settings) {
        super(leaf);
        this.settings = settings;
        // Add Preview Mode Container
        this.previewEl = this.contentEl.createDiv({ cls: 'cook-preview-view', attr: { 'style': 'display: none' } });
        // Add Source Mode Container
        this.sourceEl = this.contentEl.createDiv({ cls: 'cook-source-view', attr: { 'style': 'display: block' } });
        // Add container for CodeMirror editor
        this.editorEl = this.sourceEl.createEl('textarea', { cls: 'cook-cm-editor' });
        // Create CodeMirror Editor with specific config
        this.editor = CodeMirror.fromTextArea(this.editorEl, {
            lineNumbers: false,
            lineWrapping: true,
            scrollbarStyle: null,
            keyMap: "default",
            theme: "obsidian"
        });
    }
    onload() {
        // Save file on change
        this.editor.on('change', () => {
            this.requestSave();
        });
        // add the action to switch between source and preview mode
        this.changeModeButton = this.addAction('lines-of-text', 'Preview (Ctrl+Click to open in new pane)', (evt) => this.switchMode(evt), 17);
        // undocumented: Get the current default view mode to switch to
        let defaultViewMode = this.app.vault.getConfig('defaultViewMode');
        this.setState(Object.assign(Object.assign({}, this.getState()), { mode: defaultViewMode }), {});
    }
    getState() {
        return super.getState();
    }
    setState(state, result) {
        return super.setState(state, result).then(() => {
            if (state.mode)
                this.switchMode(state.mode);
        });
    }
    // function to switch between source and preview mode
    switchMode(arg) {
        let mode = arg;
        // if force mode not provided, switch to opposite of current mode
        if (!mode || mode instanceof MouseEvent)
            mode = this.currentView === 'source' ? 'preview' : 'source';
        if (arg instanceof MouseEvent) {
            if (obsidian.Keymap.isModEvent(arg)) {
                this.app.workspace.duplicateLeaf(this.leaf).then(() => {
                    var _a;
                    const cookLeaf = (_a = this.app.workspace.activeLeaf) === null || _a === void 0 ? void 0 : _a.view;
                    if (cookLeaf) {
                        cookLeaf.setState(Object.assign(Object.assign({}, cookLeaf.getState()), { mode: mode }), {});
                    }
                });
            }
            else {
                this.setState(Object.assign(Object.assign({}, this.getState()), { mode: mode }), {});
            }
        }
        else {
            // switch to preview mode
            if (mode === 'preview') {
                this.currentView = 'preview';
                obsidian.setIcon(this.changeModeButton, 'pencil');
                this.changeModeButton.setAttribute('aria-label', 'Edit (Ctrl+Click to edit in new pane)');
                this.renderPreview(this.recipe);
                this.previewEl.style.setProperty('display', 'block');
                this.sourceEl.style.setProperty('display', 'none');
            }
            // switch to source mode
            else {
                this.currentView = 'source';
                obsidian.setIcon(this.changeModeButton, 'lines-of-text');
                this.changeModeButton.setAttribute('aria-label', 'Preview (Ctrl+Click to open in new pane)');
                this.previewEl.style.setProperty('display', 'none');
                this.sourceEl.style.setProperty('display', 'block');
                this.editor.refresh();
            }
        }
    }
    // get the data for save
    getViewData() {
        this.data = this.editor.getValue();
        // may as well parse the recipe while we're here.
        this.recipe = CookLang.parse(this.data);
        return this.data;
    }
    // load the data into the view
    setViewData(data, clear) {
        return __awaiter(this, void 0, void 0, function* () {
            this.data = data;
            if (clear) {
                this.editor.swapDoc(CodeMirror.Doc(data, "text/x-cook"));
                this.editor.clearHistory();
            }
            this.editor.setValue(data);
            this.recipe = CookLang.parse(data);
            // if we're in preview view, also render that
            if (this.currentView === 'preview')
                this.renderPreview(this.recipe);
        });
    }
    // clear the editor, etc
    clear() {
        this.previewEl.empty();
        this.editor.setValue('');
        this.editor.clearHistory();
        this.recipe = new Recipe();
        this.data = null;
    }
    getDisplayText() {
        if (this.file)
            return this.file.basename;
        else
            return "Cooklang (no file)";
    }
    canAcceptExtension(extension) {
        return extension == 'cook';
    }
    getViewType() {
        return "cook";
    }
    // when the view is resized, refresh CodeMirror (thanks Licat!)
    onResize() {
        this.editor.refresh();
    }
    // icon for the view
    getIcon() {
        return "document-cook";
    }
    // render the preview view
    renderPreview(recipe) {
        // clear the preview before adding the rest
        this.previewEl.empty();
        // we can't render what we don't have...
        if (!recipe)
            return;
        if (this.settings.showImages) {
            // add any files following the cooklang conventions to the recipe object
            // https://cooklang.org/docs/spec/#adding-pictures
            const otherFiles = this.file.parent.children.filter(f => (f instanceof obsidian.TFile) && (f.basename == this.file.basename || f.basename.startsWith(this.file.basename + '.')) && f.name != this.file.name);
            otherFiles.forEach(f => {
                // convention specifies JPEGs and PNGs. Added GIFs as well. Why not?
                if (f.extension == "jpg" || f.extension == "jpeg" || f.extension == "png" || f.extension == "gif") {
                    // main recipe image
                    if (f.basename == this.file.basename)
                        recipe.image = f;
                    else {
                        const split = f.basename.split('.');
                        // individual step images
                        if (split.length == 2 && parseInt(split[1])) {
                            recipe.methodImages.set(parseInt(split[1]), f);
                        }
                    }
                }
            });
            // if there is a main image, put it as a banner image at the top
            if (recipe.image) {
                const img = this.previewEl.createEl('img', { cls: 'main-image' });
                img.src = this.app.vault.getResourcePath(recipe.image);
            }
        }
        if (this.settings.showIngredientList) {
            // Add the Ingredients header
            this.previewEl.createEl('h2', { cls: 'ingredients-header', text: 'Ingredients' });
            // Add the ingredients list
            const ul = this.previewEl.createEl('ul', { cls: 'ingredients' });
            recipe.ingredients.forEach(ingredient => {
                const li = ul.createEl('li');
                if (ingredient.amount !== null) {
                    li.createEl('span', { cls: 'amount', text: ingredient.amount });
                    li.appendText(' ');
                }
                if (ingredient.unit !== null) {
                    li.createEl('span', { cls: 'unit', text: ingredient.unit });
                    li.appendText(' ');
                }
                li.appendText(ingredient.name);
            });
        }
        if (this.settings.showCookwareList) {
            // Add the Cookware header
            this.previewEl.createEl('h2', { cls: 'cookware-header', text: 'Cookware' });
            // Add the Cookware list
            const ul = this.previewEl.createEl('ul', { cls: 'cookware' });
            recipe.cookware.forEach(item => {
                ul.createEl('li', { text: item.name });
            });
        }
        if (this.settings.showTotalTime) {
            let time = recipe.calculateTotalTime();
            if (time > 0) {
                // Add the Timers header
                this.previewEl.createEl('h2', { cls: 'time-header', text: 'Total Time' });
                this.previewEl.createEl('p', { cls: 'time', text: this.formatTime(time) });
            }
        }
        // add the method header
        this.previewEl.createEl('h2', { cls: 'method-header', text: 'Method' });
        // add the method list
        const mol = this.previewEl.createEl('ol', { cls: 'method' });
        let i = 1;
        recipe.method.forEach(line => {
            var _a, _b;
            const mli = mol.createEl('li');
            mli.innerHTML = line;
            if (!this.settings.showQuantitiesInline) {
                (_a = mli.querySelectorAll('.amount')) === null || _a === void 0 ? void 0 : _a.forEach(el => el.remove());
                (_b = mli.querySelectorAll('.unit')) === null || _b === void 0 ? void 0 : _b.forEach(el => el.remove());
            }
            if (this.settings.showImages && recipe.methodImages.has(i)) {
                const img = mli.createEl('img', { cls: 'method-image' });
                img.src = this.app.vault.getResourcePath(recipe.methodImages.get(i));
            }
            i++;
        });
    }
    formatTime(time) {
        let minutes = Math.floor(time / 60);
        let hours = Math.floor(minutes / 60);
        minutes = minutes % 60;
        let result = "";
        if (hours > 0)
            result += hours + " hours ";
        if (minutes > 0)
            result += minutes + " minutes ";
        return result;
    }
}

class CookLangSettings {
    constructor() {
        this.showImages = true;
        this.showIngredientList = true;
        this.showCookwareList = true;
        this.showTotalTime = true;
        this.showQuantitiesInline = false;
    }
}
class CookSettingsTab extends obsidian.PluginSettingTab {
    constructor(app, plugin) {
        super(app, plugin);
        this.plugin = plugin;
    }
    display() {
        let { containerEl } = this;
        containerEl.empty();
        new obsidian.Setting(containerEl)
            .setName('Preview Options')
            .setHeading();
        new obsidian.Setting(containerEl)
            .setName('Show images')
            .setDesc('Show images in the recipe (see https://cooklang.org/docs/spec/#adding-pictures)')
            .addToggle(toggle => toggle
            .setValue(this.plugin.settings.showImages)
            .onChange((value) => {
            this.plugin.settings.showImages = value;
            this.plugin.saveData(this.plugin.settings);
            this.plugin.reloadCookViews();
        }));
        new obsidian.Setting(containerEl)
            .setName('Show ingredient list')
            .setDesc('Show the list of ingredients at the top of the recipe')
            .addToggle(toggle => toggle
            .setValue(this.plugin.settings.showIngredientList)
            .onChange((value) => {
            this.plugin.settings.showIngredientList = value;
            this.plugin.saveData(this.plugin.settings);
            this.plugin.reloadCookViews();
        }));
        new obsidian.Setting(containerEl)
            .setName('Show cookware list')
            .setDesc('Show the list of cookware at the top of the recipe')
            .addToggle(toggle => toggle
            .setValue(this.plugin.settings.showCookwareList)
            .onChange((value) => {
            this.plugin.settings.showCookwareList = value;
            this.plugin.saveData(this.plugin.settings);
            this.plugin.reloadCookViews();
        }));
        new obsidian.Setting(containerEl)
            .setName('Show total time')
            .setDesc('Show the total of all timers at the top of the recipe')
            .addToggle(toggle => toggle
            .setValue(this.plugin.settings.showTotalTime)
            .onChange((value) => {
            this.plugin.settings.showTotalTime = value;
            this.plugin.saveData(this.plugin.settings);
            this.plugin.reloadCookViews();
        }));
        new obsidian.Setting(containerEl)
            .setName('Show quantities inline')
            .setDesc('Show the ingredient quantities inline in the recipe method')
            .addToggle(toggle => toggle
            .setValue(this.plugin.settings.showQuantitiesInline)
            .onChange((value) => {
            this.plugin.settings.showQuantitiesInline = value;
            this.plugin.saveData(this.plugin.settings);
            this.plugin.reloadCookViews();
        }));
    }
}

class CookPlugin extends obsidian.Plugin {
    constructor() {
        super(...arguments);
        this.cookFileCreator = () => __awaiter(this, void 0, void 0, function* () {
            var _a, _b;
            let newFileFolderPath = null;
            const newFileLocation = this.app.vault.getConfig('newFileLocation');
            if (!newFileLocation || newFileLocation === "root") {
                newFileFolderPath = '/';
            }
            else if (newFileLocation === "current") {
                newFileFolderPath = (_b = (_a = this.app.workspace.getActiveFile()) === null || _a === void 0 ? void 0 : _a.parent) === null || _b === void 0 ? void 0 : _b.path;
            }
            else {
                newFileFolderPath = this.app.vault.getConfig('newFileFolderPath');
            }
            if (!newFileFolderPath)
                newFileFolderPath = '/';
            else if (!newFileFolderPath.endsWith('/'))
                newFileFolderPath += '/';
            const originalPath = newFileFolderPath;
            newFileFolderPath = newFileFolderPath + 'Untitled.cook';
            let i = 0;
            while (this.app.vault.getAbstractFileByPath(newFileFolderPath)) {
                newFileFolderPath = `${originalPath}Untitled ${++i}.cook`;
            }
            const newFile = yield this.app.vault.create(newFileFolderPath, '');
            return newFile;
        });
        // function to create the view
        this.cookViewCreator = (leaf) => {
            return new CookView(leaf, this.settings);
        };
        // this function provides the icon for the document
        // I added a modification of the CookLang icon with no colours or shadows
        this.addDocumentIcon = (extension) => {
            obsidian.addIcon(`document-${extension}`, `
    <svg viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path d="M30 48C39.9411 48 48 39.9411 48 30H12C12 39.9411 20.0589 48 30 48Z" fill="currentColor"/>
    <circle cx="18" cy="18" r="4" fill="currentColor"/>
    <circle cx="42" cy="18" r="4" fill="currentColor"/>
    <circle cx="30" cy="16" r="4" fill="currentColor"/>
    </svg>
    `);
        };
    }
    onload() {
        const _super = Object.create(null, {
            onload: { get: () => super.onload }
        });
        return __awaiter(this, void 0, void 0, function* () {
            _super.onload.call(this);
            this.settings = Object.assign(new CookLangSettings(), yield this.loadData());
            // register a custom icon
            this.addDocumentIcon("cook");
            // register the view and extensions
            this.registerView("cook", this.cookViewCreator);
            this.registerExtensions(["cook"], "cook");
            this.addSettingTab(new CookSettingsTab(this.app, this));
            // commands:
            // - Create new recipe
            // - Create recipe in new pane
            // - Convert markdown file to `.cook`
            this.addCommand({
                id: "create-cook",
                name: "Create new recipe",
                callback: () => __awaiter(this, void 0, void 0, function* () {
                    const newFile = yield this.cookFileCreator();
                    this.app.workspace.getLeaf().openFile(newFile);
                })
            });
            this.addCommand({
                id: "create-cook-new-pane",
                name: "Create recipe in new pane",
                callback: () => __awaiter(this, void 0, void 0, function* () {
                    const newFile = yield this.cookFileCreator();
                    yield this.app.workspace.getLeaf(true).openFile(newFile);
                })
            });
            // register the convert to cook command
            this.addCommand({
                id: "convert-to-cook",
                name: "Convert markdown file to `.cook`",
                checkCallback: (checking) => {
                    const file = this.app.workspace.getActiveFile();
                    const isMd = file.extension === "md";
                    if (checking) {
                        return isMd;
                    }
                    else if (isMd) {
                        // replace last instance of .md with .cook
                        this.app.vault.rename(file, file.path.replace(/\.md$/, ".cook")).then(() => {
                            this.app.workspace.activeLeaf.openFile(file);
                        });
                    }
                }
            });
        });
    }
    reloadCookViews() {
        this.app.workspace.getLeavesOfType('cook').forEach(leaf => {
            if (leaf.view instanceof CookView) {
                leaf.view.settings = this.settings;
                if (leaf.view.recipe)
                    leaf.view.renderPreview(leaf.view.recipe);
            }
        });
    }
}

module.exports = CookPlugin;
//# sourceMappingURL=data:application/json;charset=utf-8;base64,[encoded data, 30440 characters]
