2641 lines
120 KiB
JavaScript
2641 lines
120 KiB
JavaScript
/*
|
|
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
|
if you want to view the source, please visit the github repository of this plugin
|
|
*/
|
|
|
|
var __create = Object.create;
|
|
var __defProp = Object.defineProperty;
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __getProtoOf = Object.getPrototypeOf;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __commonJS = (cb, mod) => function __require() {
|
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
};
|
|
var __export = (target, all) => {
|
|
for (var name in all)
|
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
};
|
|
var __copyProps = (to, from, except, desc) => {
|
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
for (let key of __getOwnPropNames(from))
|
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
}
|
|
return to;
|
|
};
|
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod));
|
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
|
|
// node_modules/dompurify/dist/purify.js
|
|
var require_purify = __commonJS({
|
|
"node_modules/dompurify/dist/purify.js"(exports, module2) {
|
|
(function(global, factory) {
|
|
typeof exports === "object" && typeof module2 !== "undefined" ? module2.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, global.DOMPurify = factory());
|
|
})(exports, function() {
|
|
"use strict";
|
|
const {
|
|
entries,
|
|
setPrototypeOf,
|
|
isFrozen,
|
|
getPrototypeOf,
|
|
getOwnPropertyDescriptor
|
|
} = Object;
|
|
let {
|
|
freeze,
|
|
seal,
|
|
create
|
|
} = Object;
|
|
let {
|
|
apply,
|
|
construct
|
|
} = typeof Reflect !== "undefined" && Reflect;
|
|
if (!freeze) {
|
|
freeze = function freeze2(x) {
|
|
return x;
|
|
};
|
|
}
|
|
if (!seal) {
|
|
seal = function seal2(x) {
|
|
return x;
|
|
};
|
|
}
|
|
if (!apply) {
|
|
apply = function apply2(fun, thisValue, args) {
|
|
return fun.apply(thisValue, args);
|
|
};
|
|
}
|
|
if (!construct) {
|
|
construct = function construct2(Func, args) {
|
|
return new Func(...args);
|
|
};
|
|
}
|
|
const arrayForEach = unapply(Array.prototype.forEach);
|
|
const arrayPop = unapply(Array.prototype.pop);
|
|
const arrayPush = unapply(Array.prototype.push);
|
|
const stringToLowerCase = unapply(String.prototype.toLowerCase);
|
|
const stringToString = unapply(String.prototype.toString);
|
|
const stringMatch = unapply(String.prototype.match);
|
|
const stringReplace = unapply(String.prototype.replace);
|
|
const stringIndexOf = unapply(String.prototype.indexOf);
|
|
const stringTrim = unapply(String.prototype.trim);
|
|
const objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty);
|
|
const regExpTest = unapply(RegExp.prototype.test);
|
|
const typeErrorCreate = unconstruct(TypeError);
|
|
function unapply(func) {
|
|
return function(thisArg) {
|
|
for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
|
args[_key - 1] = arguments[_key];
|
|
}
|
|
return apply(func, thisArg, args);
|
|
};
|
|
}
|
|
function unconstruct(func) {
|
|
return function() {
|
|
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
|
|
args[_key2] = arguments[_key2];
|
|
}
|
|
return construct(func, args);
|
|
};
|
|
}
|
|
function addToSet(set, array) {
|
|
let transformCaseFunc = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : stringToLowerCase;
|
|
if (setPrototypeOf) {
|
|
setPrototypeOf(set, null);
|
|
}
|
|
let l = array.length;
|
|
while (l--) {
|
|
let element = array[l];
|
|
if (typeof element === "string") {
|
|
const lcElement = transformCaseFunc(element);
|
|
if (lcElement !== element) {
|
|
if (!isFrozen(array)) {
|
|
array[l] = lcElement;
|
|
}
|
|
element = lcElement;
|
|
}
|
|
}
|
|
set[element] = true;
|
|
}
|
|
return set;
|
|
}
|
|
function cleanArray(array) {
|
|
for (let index = 0; index < array.length; index++) {
|
|
const isPropertyExist = objectHasOwnProperty(array, index);
|
|
if (!isPropertyExist) {
|
|
array[index] = null;
|
|
}
|
|
}
|
|
return array;
|
|
}
|
|
function clone(object) {
|
|
const newObject = create(null);
|
|
for (const [property, value] of entries(object)) {
|
|
const isPropertyExist = objectHasOwnProperty(object, property);
|
|
if (isPropertyExist) {
|
|
if (Array.isArray(value)) {
|
|
newObject[property] = cleanArray(value);
|
|
} else if (value && typeof value === "object" && value.constructor === Object) {
|
|
newObject[property] = clone(value);
|
|
} else {
|
|
newObject[property] = value;
|
|
}
|
|
}
|
|
}
|
|
return newObject;
|
|
}
|
|
function lookupGetter(object, prop) {
|
|
while (object !== null) {
|
|
const desc = getOwnPropertyDescriptor(object, prop);
|
|
if (desc) {
|
|
if (desc.get) {
|
|
return unapply(desc.get);
|
|
}
|
|
if (typeof desc.value === "function") {
|
|
return unapply(desc.value);
|
|
}
|
|
}
|
|
object = getPrototypeOf(object);
|
|
}
|
|
function fallbackValue() {
|
|
return null;
|
|
}
|
|
return fallbackValue;
|
|
}
|
|
const html$1 = freeze(["a", "abbr", "acronym", "address", "area", "article", "aside", "audio", "b", "bdi", "bdo", "big", "blink", "blockquote", "body", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "content", "data", "datalist", "dd", "decorator", "del", "details", "dfn", "dialog", "dir", "div", "dl", "dt", "element", "em", "fieldset", "figcaption", "figure", "font", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html", "i", "img", "input", "ins", "kbd", "label", "legend", "li", "main", "map", "mark", "marquee", "menu", "menuitem", "meter", "nav", "nobr", "ol", "optgroup", "option", "output", "p", "picture", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "section", "select", "shadow", "small", "source", "spacer", "span", "strike", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"]);
|
|
const svg$1 = freeze(["svg", "a", "altglyph", "altglyphdef", "altglyphitem", "animatecolor", "animatemotion", "animatetransform", "circle", "clippath", "defs", "desc", "ellipse", "filter", "font", "g", "glyph", "glyphref", "hkern", "image", "line", "lineargradient", "marker", "mask", "metadata", "mpath", "path", "pattern", "polygon", "polyline", "radialgradient", "rect", "stop", "style", "switch", "symbol", "text", "textpath", "title", "tref", "tspan", "view", "vkern"]);
|
|
const svgFilters = freeze(["feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feDropShadow", "feFlood", "feFuncA", "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology", "feOffset", "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence"]);
|
|
const svgDisallowed = freeze(["animate", "color-profile", "cursor", "discard", "font-face", "font-face-format", "font-face-name", "font-face-src", "font-face-uri", "foreignobject", "hatch", "hatchpath", "mesh", "meshgradient", "meshpatch", "meshrow", "missing-glyph", "script", "set", "solidcolor", "unknown", "use"]);
|
|
const mathMl$1 = freeze(["math", "menclose", "merror", "mfenced", "mfrac", "mglyph", "mi", "mlabeledtr", "mmultiscripts", "mn", "mo", "mover", "mpadded", "mphantom", "mroot", "mrow", "ms", "mspace", "msqrt", "mstyle", "msub", "msup", "msubsup", "mtable", "mtd", "mtext", "mtr", "munder", "munderover", "mprescripts"]);
|
|
const mathMlDisallowed = freeze(["maction", "maligngroup", "malignmark", "mlongdiv", "mscarries", "mscarry", "msgroup", "mstack", "msline", "msrow", "semantics", "annotation", "annotation-xml", "mprescripts", "none"]);
|
|
const text = freeze(["#text"]);
|
|
const html = freeze(["accept", "action", "align", "alt", "autocapitalize", "autocomplete", "autopictureinpicture", "autoplay", "background", "bgcolor", "border", "capture", "cellpadding", "cellspacing", "checked", "cite", "class", "clear", "color", "cols", "colspan", "controls", "controlslist", "coords", "crossorigin", "datetime", "decoding", "default", "dir", "disabled", "disablepictureinpicture", "disableremoteplayback", "download", "draggable", "enctype", "enterkeyhint", "face", "for", "headers", "height", "hidden", "high", "href", "hreflang", "id", "inputmode", "integrity", "ismap", "kind", "label", "lang", "list", "loading", "loop", "low", "max", "maxlength", "media", "method", "min", "minlength", "multiple", "muted", "name", "nonce", "noshade", "novalidate", "nowrap", "open", "optimum", "pattern", "placeholder", "playsinline", "popover", "popovertarget", "popovertargetaction", "poster", "preload", "pubdate", "radiogroup", "readonly", "rel", "required", "rev", "reversed", "role", "rows", "rowspan", "spellcheck", "scope", "selected", "shape", "size", "sizes", "span", "srclang", "start", "src", "srcset", "step", "style", "summary", "tabindex", "title", "translate", "type", "usemap", "valign", "value", "width", "wrap", "xmlns", "slot"]);
|
|
const svg = freeze(["accent-height", "accumulate", "additive", "alignment-baseline", "ascent", "attributename", "attributetype", "azimuth", "basefrequency", "baseline-shift", "begin", "bias", "by", "class", "clip", "clippathunits", "clip-path", "clip-rule", "color", "color-interpolation", "color-interpolation-filters", "color-profile", "color-rendering", "cx", "cy", "d", "dx", "dy", "diffuseconstant", "direction", "display", "divisor", "dur", "edgemode", "elevation", "end", "fill", "fill-opacity", "fill-rule", "filter", "filterunits", "flood-color", "flood-opacity", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "fx", "fy", "g1", "g2", "glyph-name", "glyphref", "gradientunits", "gradienttransform", "height", "href", "id", "image-rendering", "in", "in2", "k", "k1", "k2", "k3", "k4", "kerning", "keypoints", "keysplines", "keytimes", "lang", "lengthadjust", "letter-spacing", "kernelmatrix", "kernelunitlength", "lighting-color", "local", "marker-end", "marker-mid", "marker-start", "markerheight", "markerunits", "markerwidth", "maskcontentunits", "maskunits", "max", "mask", "media", "method", "mode", "min", "name", "numoctaves", "offset", "operator", "opacity", "order", "orient", "orientation", "origin", "overflow", "paint-order", "path", "pathlength", "patterncontentunits", "patterntransform", "patternunits", "points", "preservealpha", "preserveaspectratio", "primitiveunits", "r", "rx", "ry", "radius", "refx", "refy", "repeatcount", "repeatdur", "restart", "result", "rotate", "scale", "seed", "shape-rendering", "specularconstant", "specularexponent", "spreadmethod", "startoffset", "stddeviation", "stitchtiles", "stop-color", "stop-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke", "stroke-width", "style", "surfacescale", "systemlanguage", "tabindex", "targetx", "targety", "transform", "transform-origin", "text-anchor", "text-decoration", "text-rendering", "textlength", "type", "u1", "u2", "unicode", "values", "viewbox", "visibility", "version", "vert-adv-y", "vert-origin-x", "vert-origin-y", "width", "word-spacing", "wrap", "writing-mode", "xchannelselector", "ychannelselector", "x", "x1", "x2", "xmlns", "y", "y1", "y2", "z", "zoomandpan"]);
|
|
const mathMl = freeze(["accent", "accentunder", "align", "bevelled", "close", "columnsalign", "columnlines", "columnspan", "denomalign", "depth", "dir", "display", "displaystyle", "encoding", "fence", "frame", "height", "href", "id", "largeop", "length", "linethickness", "lspace", "lquote", "mathbackground", "mathcolor", "mathsize", "mathvariant", "maxsize", "minsize", "movablelimits", "notation", "numalign", "open", "rowalign", "rowlines", "rowspacing", "rowspan", "rspace", "rquote", "scriptlevel", "scriptminsize", "scriptsizemultiplier", "selection", "separator", "separators", "stretchy", "subscriptshift", "supscriptshift", "symmetric", "voffset", "width", "xmlns"]);
|
|
const xml = freeze(["xlink:href", "xml:id", "xlink:title", "xml:space", "xmlns:xlink"]);
|
|
const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm);
|
|
const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm);
|
|
const TMPLIT_EXPR = seal(/\${[\w\W]*}/gm);
|
|
const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]/);
|
|
const ARIA_ATTR = seal(/^aria-[\-\w]+$/);
|
|
const IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i);
|
|
const IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i);
|
|
const ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g);
|
|
const DOCTYPE_NAME = seal(/^html$/i);
|
|
const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i);
|
|
var EXPRESSIONS = /* @__PURE__ */ Object.freeze({
|
|
__proto__: null,
|
|
MUSTACHE_EXPR,
|
|
ERB_EXPR,
|
|
TMPLIT_EXPR,
|
|
DATA_ATTR,
|
|
ARIA_ATTR,
|
|
IS_ALLOWED_URI,
|
|
IS_SCRIPT_OR_DATA,
|
|
ATTR_WHITESPACE,
|
|
DOCTYPE_NAME,
|
|
CUSTOM_ELEMENT
|
|
});
|
|
const NODE_TYPE = {
|
|
element: 1,
|
|
attribute: 2,
|
|
text: 3,
|
|
cdataSection: 4,
|
|
entityReference: 5,
|
|
entityNode: 6,
|
|
progressingInstruction: 7,
|
|
comment: 8,
|
|
document: 9,
|
|
documentType: 10,
|
|
documentFragment: 11,
|
|
notation: 12
|
|
};
|
|
const getGlobal = function getGlobal2() {
|
|
return typeof window === "undefined" ? null : window;
|
|
};
|
|
const _createTrustedTypesPolicy = function _createTrustedTypesPolicy2(trustedTypes, purifyHostElement) {
|
|
if (typeof trustedTypes !== "object" || typeof trustedTypes.createPolicy !== "function") {
|
|
return null;
|
|
}
|
|
let suffix = null;
|
|
const ATTR_NAME = "data-tt-policy-suffix";
|
|
if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) {
|
|
suffix = purifyHostElement.getAttribute(ATTR_NAME);
|
|
}
|
|
const policyName = "dompurify" + (suffix ? "#" + suffix : "");
|
|
try {
|
|
return trustedTypes.createPolicy(policyName, {
|
|
createHTML(html2) {
|
|
return html2;
|
|
},
|
|
createScriptURL(scriptUrl) {
|
|
return scriptUrl;
|
|
}
|
|
});
|
|
} catch (_) {
|
|
console.warn("TrustedTypes policy " + policyName + " could not be created.");
|
|
return null;
|
|
}
|
|
};
|
|
function createDOMPurify() {
|
|
let window2 = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : getGlobal();
|
|
const DOMPurify2 = (root) => createDOMPurify(root);
|
|
DOMPurify2.version = "3.1.5";
|
|
DOMPurify2.removed = [];
|
|
if (!window2 || !window2.document || window2.document.nodeType !== NODE_TYPE.document) {
|
|
DOMPurify2.isSupported = false;
|
|
return DOMPurify2;
|
|
}
|
|
let {
|
|
document: document2
|
|
} = window2;
|
|
const originalDocument = document2;
|
|
const currentScript = originalDocument.currentScript;
|
|
const {
|
|
DocumentFragment,
|
|
HTMLTemplateElement,
|
|
Node,
|
|
Element,
|
|
NodeFilter,
|
|
NamedNodeMap = window2.NamedNodeMap || window2.MozNamedAttrMap,
|
|
HTMLFormElement,
|
|
DOMParser,
|
|
trustedTypes
|
|
} = window2;
|
|
const ElementPrototype = Element.prototype;
|
|
const cloneNode = lookupGetter(ElementPrototype, "cloneNode");
|
|
const getNextSibling = lookupGetter(ElementPrototype, "nextSibling");
|
|
const getChildNodes = lookupGetter(ElementPrototype, "childNodes");
|
|
const getParentNode = lookupGetter(ElementPrototype, "parentNode");
|
|
if (typeof HTMLTemplateElement === "function") {
|
|
const template = document2.createElement("template");
|
|
if (template.content && template.content.ownerDocument) {
|
|
document2 = template.content.ownerDocument;
|
|
}
|
|
}
|
|
let trustedTypesPolicy;
|
|
let emptyHTML = "";
|
|
const {
|
|
implementation,
|
|
createNodeIterator,
|
|
createDocumentFragment,
|
|
getElementsByTagName
|
|
} = document2;
|
|
const {
|
|
importNode
|
|
} = originalDocument;
|
|
let hooks = {};
|
|
DOMPurify2.isSupported = typeof entries === "function" && typeof getParentNode === "function" && implementation && implementation.createHTMLDocument !== void 0;
|
|
const {
|
|
MUSTACHE_EXPR: MUSTACHE_EXPR2,
|
|
ERB_EXPR: ERB_EXPR2,
|
|
TMPLIT_EXPR: TMPLIT_EXPR2,
|
|
DATA_ATTR: DATA_ATTR2,
|
|
ARIA_ATTR: ARIA_ATTR2,
|
|
IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA2,
|
|
ATTR_WHITESPACE: ATTR_WHITESPACE2,
|
|
CUSTOM_ELEMENT: CUSTOM_ELEMENT2
|
|
} = EXPRESSIONS;
|
|
let {
|
|
IS_ALLOWED_URI: IS_ALLOWED_URI$1
|
|
} = EXPRESSIONS;
|
|
let ALLOWED_TAGS = null;
|
|
const DEFAULT_ALLOWED_TAGS = addToSet({}, [...html$1, ...svg$1, ...svgFilters, ...mathMl$1, ...text]);
|
|
let ALLOWED_ATTR = null;
|
|
const DEFAULT_ALLOWED_ATTR = addToSet({}, [...html, ...svg, ...mathMl, ...xml]);
|
|
let CUSTOM_ELEMENT_HANDLING = Object.seal(create(null, {
|
|
tagNameCheck: {
|
|
writable: true,
|
|
configurable: false,
|
|
enumerable: true,
|
|
value: null
|
|
},
|
|
attributeNameCheck: {
|
|
writable: true,
|
|
configurable: false,
|
|
enumerable: true,
|
|
value: null
|
|
},
|
|
allowCustomizedBuiltInElements: {
|
|
writable: true,
|
|
configurable: false,
|
|
enumerable: true,
|
|
value: false
|
|
}
|
|
}));
|
|
let FORBID_TAGS = null;
|
|
let FORBID_ATTR = null;
|
|
let ALLOW_ARIA_ATTR = true;
|
|
let ALLOW_DATA_ATTR = true;
|
|
let ALLOW_UNKNOWN_PROTOCOLS = false;
|
|
let ALLOW_SELF_CLOSE_IN_ATTR = true;
|
|
let SAFE_FOR_TEMPLATES = false;
|
|
let SAFE_FOR_XML = true;
|
|
let WHOLE_DOCUMENT = false;
|
|
let SET_CONFIG = false;
|
|
let FORCE_BODY = false;
|
|
let RETURN_DOM = false;
|
|
let RETURN_DOM_FRAGMENT = false;
|
|
let RETURN_TRUSTED_TYPE = false;
|
|
let SANITIZE_DOM = true;
|
|
let SANITIZE_NAMED_PROPS = false;
|
|
const SANITIZE_NAMED_PROPS_PREFIX = "user-content-";
|
|
let KEEP_CONTENT = true;
|
|
let IN_PLACE = false;
|
|
let USE_PROFILES = {};
|
|
let FORBID_CONTENTS = null;
|
|
const DEFAULT_FORBID_CONTENTS = addToSet({}, ["annotation-xml", "audio", "colgroup", "desc", "foreignobject", "head", "iframe", "math", "mi", "mn", "mo", "ms", "mtext", "noembed", "noframes", "noscript", "plaintext", "script", "style", "svg", "template", "thead", "title", "video", "xmp"]);
|
|
let DATA_URI_TAGS = null;
|
|
const DEFAULT_DATA_URI_TAGS = addToSet({}, ["audio", "video", "img", "source", "image", "track"]);
|
|
let URI_SAFE_ATTRIBUTES = null;
|
|
const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ["alt", "class", "for", "id", "label", "name", "pattern", "placeholder", "role", "summary", "title", "value", "style", "xmlns"]);
|
|
const MATHML_NAMESPACE = "http://www.w3.org/1998/Math/MathML";
|
|
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
|
|
let NAMESPACE = HTML_NAMESPACE;
|
|
let IS_EMPTY_INPUT = false;
|
|
let ALLOWED_NAMESPACES = null;
|
|
const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString);
|
|
let PARSER_MEDIA_TYPE = null;
|
|
const SUPPORTED_PARSER_MEDIA_TYPES = ["application/xhtml+xml", "text/html"];
|
|
const DEFAULT_PARSER_MEDIA_TYPE = "text/html";
|
|
let transformCaseFunc = null;
|
|
let CONFIG = null;
|
|
const formElement = document2.createElement("form");
|
|
const isRegexOrFunction = function isRegexOrFunction2(testValue) {
|
|
return testValue instanceof RegExp || testValue instanceof Function;
|
|
};
|
|
const _parseConfig = function _parseConfig2() {
|
|
let cfg = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {};
|
|
if (CONFIG && CONFIG === cfg) {
|
|
return;
|
|
}
|
|
if (!cfg || typeof cfg !== "object") {
|
|
cfg = {};
|
|
}
|
|
cfg = clone(cfg);
|
|
PARSER_MEDIA_TYPE = SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1 ? DEFAULT_PARSER_MEDIA_TYPE : cfg.PARSER_MEDIA_TYPE;
|
|
transformCaseFunc = PARSER_MEDIA_TYPE === "application/xhtml+xml" ? stringToString : stringToLowerCase;
|
|
ALLOWED_TAGS = objectHasOwnProperty(cfg, "ALLOWED_TAGS") ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS;
|
|
ALLOWED_ATTR = objectHasOwnProperty(cfg, "ALLOWED_ATTR") ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR;
|
|
ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, "ALLOWED_NAMESPACES") ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES;
|
|
URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, "ADD_URI_SAFE_ATTR") ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR, transformCaseFunc) : DEFAULT_URI_SAFE_ATTRIBUTES;
|
|
DATA_URI_TAGS = objectHasOwnProperty(cfg, "ADD_DATA_URI_TAGS") ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS, transformCaseFunc) : DEFAULT_DATA_URI_TAGS;
|
|
FORBID_CONTENTS = objectHasOwnProperty(cfg, "FORBID_CONTENTS") ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS;
|
|
FORBID_TAGS = objectHasOwnProperty(cfg, "FORBID_TAGS") ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : {};
|
|
FORBID_ATTR = objectHasOwnProperty(cfg, "FORBID_ATTR") ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : {};
|
|
USE_PROFILES = objectHasOwnProperty(cfg, "USE_PROFILES") ? cfg.USE_PROFILES : false;
|
|
ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false;
|
|
ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false;
|
|
ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false;
|
|
ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false;
|
|
SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false;
|
|
SAFE_FOR_XML = cfg.SAFE_FOR_XML !== false;
|
|
WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false;
|
|
RETURN_DOM = cfg.RETURN_DOM || false;
|
|
RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false;
|
|
RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false;
|
|
FORCE_BODY = cfg.FORCE_BODY || false;
|
|
SANITIZE_DOM = cfg.SANITIZE_DOM !== false;
|
|
SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false;
|
|
KEEP_CONTENT = cfg.KEEP_CONTENT !== false;
|
|
IN_PLACE = cfg.IN_PLACE || false;
|
|
IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI;
|
|
NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE;
|
|
CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {};
|
|
if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) {
|
|
CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck;
|
|
}
|
|
if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) {
|
|
CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck;
|
|
}
|
|
if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === "boolean") {
|
|
CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements;
|
|
}
|
|
if (SAFE_FOR_TEMPLATES) {
|
|
ALLOW_DATA_ATTR = false;
|
|
}
|
|
if (RETURN_DOM_FRAGMENT) {
|
|
RETURN_DOM = true;
|
|
}
|
|
if (USE_PROFILES) {
|
|
ALLOWED_TAGS = addToSet({}, text);
|
|
ALLOWED_ATTR = [];
|
|
if (USE_PROFILES.html === true) {
|
|
addToSet(ALLOWED_TAGS, html$1);
|
|
addToSet(ALLOWED_ATTR, html);
|
|
}
|
|
if (USE_PROFILES.svg === true) {
|
|
addToSet(ALLOWED_TAGS, svg$1);
|
|
addToSet(ALLOWED_ATTR, svg);
|
|
addToSet(ALLOWED_ATTR, xml);
|
|
}
|
|
if (USE_PROFILES.svgFilters === true) {
|
|
addToSet(ALLOWED_TAGS, svgFilters);
|
|
addToSet(ALLOWED_ATTR, svg);
|
|
addToSet(ALLOWED_ATTR, xml);
|
|
}
|
|
if (USE_PROFILES.mathMl === true) {
|
|
addToSet(ALLOWED_TAGS, mathMl$1);
|
|
addToSet(ALLOWED_ATTR, mathMl);
|
|
addToSet(ALLOWED_ATTR, xml);
|
|
}
|
|
}
|
|
if (cfg.ADD_TAGS) {
|
|
if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
|
|
ALLOWED_TAGS = clone(ALLOWED_TAGS);
|
|
}
|
|
addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);
|
|
}
|
|
if (cfg.ADD_ATTR) {
|
|
if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
|
|
ALLOWED_ATTR = clone(ALLOWED_ATTR);
|
|
}
|
|
addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
|
|
}
|
|
if (cfg.ADD_URI_SAFE_ATTR) {
|
|
addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);
|
|
}
|
|
if (cfg.FORBID_CONTENTS) {
|
|
if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
|
|
FORBID_CONTENTS = clone(FORBID_CONTENTS);
|
|
}
|
|
addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);
|
|
}
|
|
if (KEEP_CONTENT) {
|
|
ALLOWED_TAGS["#text"] = true;
|
|
}
|
|
if (WHOLE_DOCUMENT) {
|
|
addToSet(ALLOWED_TAGS, ["html", "head", "body"]);
|
|
}
|
|
if (ALLOWED_TAGS.table) {
|
|
addToSet(ALLOWED_TAGS, ["tbody"]);
|
|
delete FORBID_TAGS.tbody;
|
|
}
|
|
if (cfg.TRUSTED_TYPES_POLICY) {
|
|
if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== "function") {
|
|
throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');
|
|
}
|
|
if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== "function") {
|
|
throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');
|
|
}
|
|
trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;
|
|
emptyHTML = trustedTypesPolicy.createHTML("");
|
|
} else {
|
|
if (trustedTypesPolicy === void 0) {
|
|
trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
|
|
}
|
|
if (trustedTypesPolicy !== null && typeof emptyHTML === "string") {
|
|
emptyHTML = trustedTypesPolicy.createHTML("");
|
|
}
|
|
}
|
|
if (freeze) {
|
|
freeze(cfg);
|
|
}
|
|
CONFIG = cfg;
|
|
};
|
|
const MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ["mi", "mo", "mn", "ms", "mtext"]);
|
|
const HTML_INTEGRATION_POINTS = addToSet({}, ["foreignobject", "annotation-xml"]);
|
|
const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, ["title", "style", "font", "a", "script"]);
|
|
const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]);
|
|
const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]);
|
|
const _checkValidNamespace = function _checkValidNamespace2(element) {
|
|
let parent = getParentNode(element);
|
|
if (!parent || !parent.tagName) {
|
|
parent = {
|
|
namespaceURI: NAMESPACE,
|
|
tagName: "template"
|
|
};
|
|
}
|
|
const tagName = stringToLowerCase(element.tagName);
|
|
const parentTagName = stringToLowerCase(parent.tagName);
|
|
if (!ALLOWED_NAMESPACES[element.namespaceURI]) {
|
|
return false;
|
|
}
|
|
if (element.namespaceURI === SVG_NAMESPACE) {
|
|
if (parent.namespaceURI === HTML_NAMESPACE) {
|
|
return tagName === "svg";
|
|
}
|
|
if (parent.namespaceURI === MATHML_NAMESPACE) {
|
|
return tagName === "svg" && (parentTagName === "annotation-xml" || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
|
|
}
|
|
return Boolean(ALL_SVG_TAGS[tagName]);
|
|
}
|
|
if (element.namespaceURI === MATHML_NAMESPACE) {
|
|
if (parent.namespaceURI === HTML_NAMESPACE) {
|
|
return tagName === "math";
|
|
}
|
|
if (parent.namespaceURI === SVG_NAMESPACE) {
|
|
return tagName === "math" && HTML_INTEGRATION_POINTS[parentTagName];
|
|
}
|
|
return Boolean(ALL_MATHML_TAGS[tagName]);
|
|
}
|
|
if (element.namespaceURI === HTML_NAMESPACE) {
|
|
if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
|
|
return false;
|
|
}
|
|
if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
|
|
return false;
|
|
}
|
|
return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
|
|
}
|
|
if (PARSER_MEDIA_TYPE === "application/xhtml+xml" && ALLOWED_NAMESPACES[element.namespaceURI]) {
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
const _forceRemove = function _forceRemove2(node) {
|
|
arrayPush(DOMPurify2.removed, {
|
|
element: node
|
|
});
|
|
try {
|
|
node.parentNode.removeChild(node);
|
|
} catch (_) {
|
|
node.remove();
|
|
}
|
|
};
|
|
const _removeAttribute = function _removeAttribute2(name, node) {
|
|
try {
|
|
arrayPush(DOMPurify2.removed, {
|
|
attribute: node.getAttributeNode(name),
|
|
from: node
|
|
});
|
|
} catch (_) {
|
|
arrayPush(DOMPurify2.removed, {
|
|
attribute: null,
|
|
from: node
|
|
});
|
|
}
|
|
node.removeAttribute(name);
|
|
if (name === "is" && !ALLOWED_ATTR[name]) {
|
|
if (RETURN_DOM || RETURN_DOM_FRAGMENT) {
|
|
try {
|
|
_forceRemove(node);
|
|
} catch (_) {
|
|
}
|
|
} else {
|
|
try {
|
|
node.setAttribute(name, "");
|
|
} catch (_) {
|
|
}
|
|
}
|
|
}
|
|
};
|
|
const _initDocument = function _initDocument2(dirty) {
|
|
let doc = null;
|
|
let leadingWhitespace = null;
|
|
if (FORCE_BODY) {
|
|
dirty = "<remove></remove>" + dirty;
|
|
} else {
|
|
const matches = stringMatch(dirty, /^[\r\n\t ]+/);
|
|
leadingWhitespace = matches && matches[0];
|
|
}
|
|
if (PARSER_MEDIA_TYPE === "application/xhtml+xml" && NAMESPACE === HTML_NAMESPACE) {
|
|
dirty = '<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>' + dirty + "</body></html>";
|
|
}
|
|
const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty;
|
|
if (NAMESPACE === HTML_NAMESPACE) {
|
|
try {
|
|
doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE);
|
|
} catch (_) {
|
|
}
|
|
}
|
|
if (!doc || !doc.documentElement) {
|
|
doc = implementation.createDocument(NAMESPACE, "template", null);
|
|
try {
|
|
doc.documentElement.innerHTML = IS_EMPTY_INPUT ? emptyHTML : dirtyPayload;
|
|
} catch (_) {
|
|
}
|
|
}
|
|
const body = doc.body || doc.documentElement;
|
|
if (dirty && leadingWhitespace) {
|
|
body.insertBefore(document2.createTextNode(leadingWhitespace), body.childNodes[0] || null);
|
|
}
|
|
if (NAMESPACE === HTML_NAMESPACE) {
|
|
return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? "html" : "body")[0];
|
|
}
|
|
return WHOLE_DOCUMENT ? doc.documentElement : body;
|
|
};
|
|
const _createNodeIterator = function _createNodeIterator2(root) {
|
|
return createNodeIterator.call(root.ownerDocument || root, root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
|
|
};
|
|
const _isClobbered = function _isClobbered2(elm) {
|
|
return elm instanceof HTMLFormElement && (typeof elm.nodeName !== "string" || typeof elm.textContent !== "string" || typeof elm.removeChild !== "function" || !(elm.attributes instanceof NamedNodeMap) || typeof elm.removeAttribute !== "function" || typeof elm.setAttribute !== "function" || typeof elm.namespaceURI !== "string" || typeof elm.insertBefore !== "function" || typeof elm.hasChildNodes !== "function");
|
|
};
|
|
const _isNode = function _isNode2(object) {
|
|
return typeof Node === "function" && object instanceof Node;
|
|
};
|
|
const _executeHook = function _executeHook2(entryPoint, currentNode, data) {
|
|
if (!hooks[entryPoint]) {
|
|
return;
|
|
}
|
|
arrayForEach(hooks[entryPoint], (hook) => {
|
|
hook.call(DOMPurify2, currentNode, data, CONFIG);
|
|
});
|
|
};
|
|
const _sanitizeElements = function _sanitizeElements2(currentNode) {
|
|
let content = null;
|
|
_executeHook("beforeSanitizeElements", currentNode, null);
|
|
if (_isClobbered(currentNode)) {
|
|
_forceRemove(currentNode);
|
|
return true;
|
|
}
|
|
const tagName = transformCaseFunc(currentNode.nodeName);
|
|
_executeHook("uponSanitizeElement", currentNode, {
|
|
tagName,
|
|
allowedTags: ALLOWED_TAGS
|
|
});
|
|
if (currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w]/g, currentNode.innerHTML) && regExpTest(/<[/\w]/g, currentNode.textContent)) {
|
|
_forceRemove(currentNode);
|
|
return true;
|
|
}
|
|
if (currentNode.nodeType === NODE_TYPE.progressingInstruction) {
|
|
_forceRemove(currentNode);
|
|
return true;
|
|
}
|
|
if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(/<[/\w]/g, currentNode.data)) {
|
|
_forceRemove(currentNode);
|
|
return true;
|
|
}
|
|
if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
|
|
if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
|
|
if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
|
|
return false;
|
|
}
|
|
if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
|
|
return false;
|
|
}
|
|
}
|
|
if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
|
|
const parentNode = getParentNode(currentNode) || currentNode.parentNode;
|
|
const childNodes = getChildNodes(currentNode) || currentNode.childNodes;
|
|
if (childNodes && parentNode) {
|
|
const childCount = childNodes.length;
|
|
for (let i = childCount - 1; i >= 0; --i) {
|
|
const childClone = cloneNode(childNodes[i], true);
|
|
childClone.__removalCount = (currentNode.__removalCount || 0) + 1;
|
|
parentNode.insertBefore(childClone, getNextSibling(currentNode));
|
|
}
|
|
}
|
|
}
|
|
_forceRemove(currentNode);
|
|
return true;
|
|
}
|
|
if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) {
|
|
_forceRemove(currentNode);
|
|
return true;
|
|
}
|
|
if ((tagName === "noscript" || tagName === "noembed" || tagName === "noframes") && regExpTest(/<\/no(script|embed|frames)/i, currentNode.innerHTML)) {
|
|
_forceRemove(currentNode);
|
|
return true;
|
|
}
|
|
if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {
|
|
content = currentNode.textContent;
|
|
arrayForEach([MUSTACHE_EXPR2, ERB_EXPR2, TMPLIT_EXPR2], (expr) => {
|
|
content = stringReplace(content, expr, " ");
|
|
});
|
|
if (currentNode.textContent !== content) {
|
|
arrayPush(DOMPurify2.removed, {
|
|
element: currentNode.cloneNode()
|
|
});
|
|
currentNode.textContent = content;
|
|
}
|
|
}
|
|
_executeHook("afterSanitizeElements", currentNode, null);
|
|
return false;
|
|
};
|
|
const _isValidAttribute = function _isValidAttribute2(lcTag, lcName, value) {
|
|
if (SANITIZE_DOM && (lcName === "id" || lcName === "name") && (value in document2 || value in formElement)) {
|
|
return false;
|
|
}
|
|
if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR2, lcName))
|
|
;
|
|
else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR2, lcName))
|
|
;
|
|
else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) {
|
|
if (_isBasicCustomElement(lcTag) && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag)) && (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName) || CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName)) || lcName === "is" && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(value)))
|
|
;
|
|
else {
|
|
return false;
|
|
}
|
|
} else if (URI_SAFE_ATTRIBUTES[lcName])
|
|
;
|
|
else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE2, "")))
|
|
;
|
|
else if ((lcName === "src" || lcName === "xlink:href" || lcName === "href") && lcTag !== "script" && stringIndexOf(value, "data:") === 0 && DATA_URI_TAGS[lcTag])
|
|
;
|
|
else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA2, stringReplace(value, ATTR_WHITESPACE2, "")))
|
|
;
|
|
else if (value) {
|
|
return false;
|
|
} else
|
|
;
|
|
return true;
|
|
};
|
|
const _isBasicCustomElement = function _isBasicCustomElement2(tagName) {
|
|
return tagName !== "annotation-xml" && stringMatch(tagName, CUSTOM_ELEMENT2);
|
|
};
|
|
const _sanitizeAttributes = function _sanitizeAttributes2(currentNode) {
|
|
_executeHook("beforeSanitizeAttributes", currentNode, null);
|
|
const {
|
|
attributes
|
|
} = currentNode;
|
|
if (!attributes) {
|
|
return;
|
|
}
|
|
const hookEvent = {
|
|
attrName: "",
|
|
attrValue: "",
|
|
keepAttr: true,
|
|
allowedAttributes: ALLOWED_ATTR
|
|
};
|
|
let l = attributes.length;
|
|
while (l--) {
|
|
const attr = attributes[l];
|
|
const {
|
|
name,
|
|
namespaceURI,
|
|
value: attrValue
|
|
} = attr;
|
|
const lcName = transformCaseFunc(name);
|
|
let value = name === "value" ? attrValue : stringTrim(attrValue);
|
|
hookEvent.attrName = lcName;
|
|
hookEvent.attrValue = value;
|
|
hookEvent.keepAttr = true;
|
|
hookEvent.forceKeepAttr = void 0;
|
|
_executeHook("uponSanitizeAttribute", currentNode, hookEvent);
|
|
value = hookEvent.attrValue;
|
|
if (hookEvent.forceKeepAttr) {
|
|
continue;
|
|
}
|
|
_removeAttribute(name, currentNode);
|
|
if (!hookEvent.keepAttr) {
|
|
continue;
|
|
}
|
|
if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) {
|
|
_removeAttribute(name, currentNode);
|
|
continue;
|
|
}
|
|
if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|title)/i, value)) {
|
|
_removeAttribute(name, currentNode);
|
|
continue;
|
|
}
|
|
if (SAFE_FOR_TEMPLATES) {
|
|
arrayForEach([MUSTACHE_EXPR2, ERB_EXPR2, TMPLIT_EXPR2], (expr) => {
|
|
value = stringReplace(value, expr, " ");
|
|
});
|
|
}
|
|
const lcTag = transformCaseFunc(currentNode.nodeName);
|
|
if (!_isValidAttribute(lcTag, lcName, value)) {
|
|
continue;
|
|
}
|
|
if (SANITIZE_NAMED_PROPS && (lcName === "id" || lcName === "name")) {
|
|
_removeAttribute(name, currentNode);
|
|
value = SANITIZE_NAMED_PROPS_PREFIX + value;
|
|
}
|
|
if (trustedTypesPolicy && typeof trustedTypes === "object" && typeof trustedTypes.getAttributeType === "function") {
|
|
if (namespaceURI)
|
|
;
|
|
else {
|
|
switch (trustedTypes.getAttributeType(lcTag, lcName)) {
|
|
case "TrustedHTML": {
|
|
value = trustedTypesPolicy.createHTML(value);
|
|
break;
|
|
}
|
|
case "TrustedScriptURL": {
|
|
value = trustedTypesPolicy.createScriptURL(value);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
try {
|
|
if (namespaceURI) {
|
|
currentNode.setAttributeNS(namespaceURI, name, value);
|
|
} else {
|
|
currentNode.setAttribute(name, value);
|
|
}
|
|
if (_isClobbered(currentNode)) {
|
|
_forceRemove(currentNode);
|
|
} else {
|
|
arrayPop(DOMPurify2.removed);
|
|
}
|
|
} catch (_) {
|
|
}
|
|
}
|
|
_executeHook("afterSanitizeAttributes", currentNode, null);
|
|
};
|
|
const _sanitizeShadowDOM = function _sanitizeShadowDOM2(fragment) {
|
|
let shadowNode = null;
|
|
const shadowIterator = _createNodeIterator(fragment);
|
|
_executeHook("beforeSanitizeShadowDOM", fragment, null);
|
|
while (shadowNode = shadowIterator.nextNode()) {
|
|
_executeHook("uponSanitizeShadowNode", shadowNode, null);
|
|
if (_sanitizeElements(shadowNode)) {
|
|
continue;
|
|
}
|
|
if (shadowNode.content instanceof DocumentFragment) {
|
|
_sanitizeShadowDOM2(shadowNode.content);
|
|
}
|
|
_sanitizeAttributes(shadowNode);
|
|
}
|
|
_executeHook("afterSanitizeShadowDOM", fragment, null);
|
|
};
|
|
DOMPurify2.sanitize = function(dirty) {
|
|
let cfg = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {};
|
|
let body = null;
|
|
let importedNode = null;
|
|
let currentNode = null;
|
|
let returnNode = null;
|
|
IS_EMPTY_INPUT = !dirty;
|
|
if (IS_EMPTY_INPUT) {
|
|
dirty = "<!-->";
|
|
}
|
|
if (typeof dirty !== "string" && !_isNode(dirty)) {
|
|
if (typeof dirty.toString === "function") {
|
|
dirty = dirty.toString();
|
|
if (typeof dirty !== "string") {
|
|
throw typeErrorCreate("dirty is not a string, aborting");
|
|
}
|
|
} else {
|
|
throw typeErrorCreate("toString is not a function");
|
|
}
|
|
}
|
|
if (!DOMPurify2.isSupported) {
|
|
return dirty;
|
|
}
|
|
if (!SET_CONFIG) {
|
|
_parseConfig(cfg);
|
|
}
|
|
DOMPurify2.removed = [];
|
|
if (typeof dirty === "string") {
|
|
IN_PLACE = false;
|
|
}
|
|
if (IN_PLACE) {
|
|
if (dirty.nodeName) {
|
|
const tagName = transformCaseFunc(dirty.nodeName);
|
|
if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
|
|
throw typeErrorCreate("root node is forbidden and cannot be sanitized in-place");
|
|
}
|
|
}
|
|
} else if (dirty instanceof Node) {
|
|
body = _initDocument("<!---->");
|
|
importedNode = body.ownerDocument.importNode(dirty, true);
|
|
if (importedNode.nodeType === NODE_TYPE.element && importedNode.nodeName === "BODY") {
|
|
body = importedNode;
|
|
} else if (importedNode.nodeName === "HTML") {
|
|
body = importedNode;
|
|
} else {
|
|
body.appendChild(importedNode);
|
|
}
|
|
} else {
|
|
if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT && dirty.indexOf("<") === -1) {
|
|
return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty;
|
|
}
|
|
body = _initDocument(dirty);
|
|
if (!body) {
|
|
return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : "";
|
|
}
|
|
}
|
|
if (body && FORCE_BODY) {
|
|
_forceRemove(body.firstChild);
|
|
}
|
|
const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body);
|
|
while (currentNode = nodeIterator.nextNode()) {
|
|
if (_sanitizeElements(currentNode)) {
|
|
continue;
|
|
}
|
|
if (currentNode.content instanceof DocumentFragment) {
|
|
_sanitizeShadowDOM(currentNode.content);
|
|
}
|
|
_sanitizeAttributes(currentNode);
|
|
}
|
|
if (IN_PLACE) {
|
|
return dirty;
|
|
}
|
|
if (RETURN_DOM) {
|
|
if (RETURN_DOM_FRAGMENT) {
|
|
returnNode = createDocumentFragment.call(body.ownerDocument);
|
|
while (body.firstChild) {
|
|
returnNode.appendChild(body.firstChild);
|
|
}
|
|
} else {
|
|
returnNode = body;
|
|
}
|
|
if (ALLOWED_ATTR.shadowroot || ALLOWED_ATTR.shadowrootmode) {
|
|
returnNode = importNode.call(originalDocument, returnNode, true);
|
|
}
|
|
return returnNode;
|
|
}
|
|
let serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML;
|
|
if (WHOLE_DOCUMENT && ALLOWED_TAGS["!doctype"] && body.ownerDocument && body.ownerDocument.doctype && body.ownerDocument.doctype.name && regExpTest(DOCTYPE_NAME, body.ownerDocument.doctype.name)) {
|
|
serializedHTML = "<!DOCTYPE " + body.ownerDocument.doctype.name + ">\n" + serializedHTML;
|
|
}
|
|
if (SAFE_FOR_TEMPLATES) {
|
|
arrayForEach([MUSTACHE_EXPR2, ERB_EXPR2, TMPLIT_EXPR2], (expr) => {
|
|
serializedHTML = stringReplace(serializedHTML, expr, " ");
|
|
});
|
|
}
|
|
return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML;
|
|
};
|
|
DOMPurify2.setConfig = function() {
|
|
let cfg = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {};
|
|
_parseConfig(cfg);
|
|
SET_CONFIG = true;
|
|
};
|
|
DOMPurify2.clearConfig = function() {
|
|
CONFIG = null;
|
|
SET_CONFIG = false;
|
|
};
|
|
DOMPurify2.isValidAttribute = function(tag, attr, value) {
|
|
if (!CONFIG) {
|
|
_parseConfig({});
|
|
}
|
|
const lcTag = transformCaseFunc(tag);
|
|
const lcName = transformCaseFunc(attr);
|
|
return _isValidAttribute(lcTag, lcName, value);
|
|
};
|
|
DOMPurify2.addHook = function(entryPoint, hookFunction) {
|
|
if (typeof hookFunction !== "function") {
|
|
return;
|
|
}
|
|
hooks[entryPoint] = hooks[entryPoint] || [];
|
|
arrayPush(hooks[entryPoint], hookFunction);
|
|
};
|
|
DOMPurify2.removeHook = function(entryPoint) {
|
|
if (hooks[entryPoint]) {
|
|
return arrayPop(hooks[entryPoint]);
|
|
}
|
|
};
|
|
DOMPurify2.removeHooks = function(entryPoint) {
|
|
if (hooks[entryPoint]) {
|
|
hooks[entryPoint] = [];
|
|
}
|
|
};
|
|
DOMPurify2.removeAllHooks = function() {
|
|
hooks = {};
|
|
};
|
|
return DOMPurify2;
|
|
}
|
|
var purify = createDOMPurify();
|
|
return purify;
|
|
});
|
|
}
|
|
});
|
|
|
|
// src/main.ts
|
|
var main_exports = {};
|
|
__export(main_exports, {
|
|
default: () => Khoj
|
|
});
|
|
module.exports = __toCommonJS(main_exports);
|
|
var import_obsidian6 = require("obsidian");
|
|
|
|
// src/settings.ts
|
|
var import_obsidian2 = require("obsidian");
|
|
|
|
// src/utils.ts
|
|
var import_obsidian = require("obsidian");
|
|
function fileExtensionToMimeType(extension) {
|
|
switch (extension) {
|
|
case "pdf":
|
|
return "application/pdf";
|
|
case "png":
|
|
return "image/png";
|
|
case "jpg":
|
|
case "jpeg":
|
|
return "image/jpeg";
|
|
case "md":
|
|
case "markdown":
|
|
return "text/markdown";
|
|
case "org":
|
|
return "text/org";
|
|
default:
|
|
return "text/plain";
|
|
}
|
|
}
|
|
function filenameToMimeType(filename) {
|
|
switch (filename.extension) {
|
|
case "pdf":
|
|
return "application/pdf";
|
|
case "png":
|
|
return "image/png";
|
|
case "jpg":
|
|
case "jpeg":
|
|
return "image/jpeg";
|
|
case "md":
|
|
case "markdown":
|
|
return "text/markdown";
|
|
case "org":
|
|
return "text/org";
|
|
default:
|
|
console.warn(`Unknown file type: ${filename.extension}. Defaulting to text/plain.`);
|
|
return "text/plain";
|
|
}
|
|
}
|
|
var supportedImageFilesTypes = ["png", "jpg", "jpeg"];
|
|
var supportedBinaryFileTypes = ["pdf"].concat(supportedImageFilesTypes);
|
|
var supportedFileTypes = ["md", "markdown"].concat(supportedBinaryFileTypes);
|
|
async function updateContentIndex(vault, setting, lastSync, regenerate = false) {
|
|
var _a;
|
|
console.log(`Khoj: Updating Khoj content index...`);
|
|
const files = vault.getFiles().filter((file) => supportedFileTypes.includes(file.extension));
|
|
let countOfFilesToIndex = 0;
|
|
let countOfFilesToDelete = 0;
|
|
lastSync = lastSync.size > 0 ? lastSync : /* @__PURE__ */ new Map();
|
|
const fileData = [];
|
|
for (const file of files) {
|
|
if (!regenerate && file.stat.mtime < ((_a = lastSync.get(file)) != null ? _a : 0)) {
|
|
continue;
|
|
}
|
|
countOfFilesToIndex++;
|
|
const encoding = supportedBinaryFileTypes.includes(file.extension) ? "binary" : "utf8";
|
|
const mimeType = fileExtensionToMimeType(file.extension) + (encoding === "utf8" ? "; charset=UTF-8" : "");
|
|
const fileContent = encoding == "binary" ? await vault.readBinary(file) : await vault.read(file);
|
|
fileData.push({ blob: new Blob([fileContent], { type: mimeType }), path: file.path });
|
|
}
|
|
let filesToDelete = [];
|
|
for (const lastSyncedFile of lastSync.keys()) {
|
|
if (!files.includes(lastSyncedFile)) {
|
|
countOfFilesToDelete++;
|
|
let fileObj = new Blob([""], { type: filenameToMimeType(lastSyncedFile) });
|
|
fileData.push({ blob: fileObj, path: lastSyncedFile.path });
|
|
filesToDelete.push(lastSyncedFile);
|
|
}
|
|
}
|
|
let responses = [];
|
|
let error_message = null;
|
|
for (let i = 0; i < fileData.length; i += 1e3) {
|
|
const filesGroup = fileData.slice(i, i + 1e3);
|
|
const formData = new FormData();
|
|
filesGroup.forEach((fileItem) => {
|
|
formData.append("files", fileItem.blob, fileItem.path);
|
|
});
|
|
const response = await fetch(`${setting.khojUrl}/api/v1/index/update?force=${regenerate}&client=obsidian`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Authorization": `Bearer ${setting.khojApiKey}`
|
|
},
|
|
body: formData
|
|
});
|
|
if (!response.ok) {
|
|
if (response.status === 429) {
|
|
error_message = `\u2757\uFE0FFailed to sync your content with Khoj server. Requests were throttled. Upgrade your subscription or try again later.`;
|
|
break;
|
|
} else if (response.status === 404) {
|
|
error_message = `\u2757\uFE0FCould not connect to Khoj server. Ensure you can connect to it.`;
|
|
break;
|
|
} else {
|
|
error_message = `\u2757\uFE0FFailed to sync your content with Khoj server. Raise issue on Khoj Discord or Github
|
|
Error: ${response.statusText}`;
|
|
}
|
|
} else {
|
|
responses.push(await response.text());
|
|
}
|
|
}
|
|
files.filter((file) => responses.find((response) => response.includes(file.path))).reduce((newSync, file) => {
|
|
newSync.set(file, new Date().getTime());
|
|
return newSync;
|
|
}, lastSync);
|
|
filesToDelete.filter((file) => responses.find((response) => response.includes(file.path))).forEach((file) => lastSync.delete(file));
|
|
if (error_message) {
|
|
new import_obsidian.Notice(error_message);
|
|
} else {
|
|
console.log(`\u2705 Refreshed Khoj content index. Updated: ${countOfFilesToIndex} files, Deleted: ${countOfFilesToDelete} files.`);
|
|
}
|
|
return lastSync;
|
|
}
|
|
async function createNote(name, newLeaf = false) {
|
|
var _a, _b;
|
|
try {
|
|
let pathPrefix;
|
|
switch (this.app.vault.getConfig("newFileLocation")) {
|
|
case "current":
|
|
pathPrefix = ((_b = (_a = this.app.workspace.getActiveFile()) == null ? void 0 : _a.parent.path) != null ? _b : "") + "/";
|
|
break;
|
|
case "folder":
|
|
pathPrefix = this.app.vault.getConfig("newFileFolderPath") + "/";
|
|
break;
|
|
default:
|
|
pathPrefix = "";
|
|
break;
|
|
}
|
|
await this.app.workspace.openLinkText(`${pathPrefix}${name}.md`, "", newLeaf);
|
|
} catch (e) {
|
|
console.error("Khoj: Could not create note.\n" + e.message);
|
|
throw e;
|
|
}
|
|
}
|
|
async function createNoteAndCloseModal(query, modal, opt) {
|
|
try {
|
|
await createNote(query, opt == null ? void 0 : opt.newLeaf);
|
|
} catch (e) {
|
|
new import_obsidian.Notice(e.message);
|
|
return;
|
|
}
|
|
modal.close();
|
|
}
|
|
async function canConnectToBackend(khojUrl, khojApiKey, showNotice = false) {
|
|
let connectedToBackend = false;
|
|
let userInfo = null;
|
|
if (!!khojUrl) {
|
|
let headers = !!khojApiKey ? { "Authorization": `Bearer ${khojApiKey}` } : void 0;
|
|
try {
|
|
let response = await (0, import_obsidian.request)({ url: `${khojUrl}/api/v1/user`, method: "GET", headers });
|
|
connectedToBackend = true;
|
|
userInfo = JSON.parse(response);
|
|
} catch (error) {
|
|
connectedToBackend = false;
|
|
console.log(`Khoj connection error:
|
|
|
|
${error}`);
|
|
}
|
|
;
|
|
}
|
|
let statusMessage = getBackendStatusMessage(connectedToBackend, userInfo == null ? void 0 : userInfo.email, khojUrl, khojApiKey);
|
|
if (showNotice)
|
|
new import_obsidian.Notice(statusMessage);
|
|
return { connectedToBackend, statusMessage, userInfo };
|
|
}
|
|
function getBackendStatusMessage(connectedToServer, userEmail, khojUrl, khojApiKey) {
|
|
if (!khojApiKey && khojUrl === "https://app.khoj.dev")
|
|
return `\u{1F308} Welcome to Khoj! Get your API key from ${khojUrl}/config#clients and set it in the Khoj plugin settings on Obsidian`;
|
|
if (!connectedToServer)
|
|
return `\u2757\uFE0FCould not connect to Khoj at ${khojUrl}. Ensure your can access it`;
|
|
else if (!userEmail)
|
|
return `\u2705 Connected to Khoj. \u2757\uFE0FGet a valid API key from ${khojUrl}/config#clients to log in`;
|
|
else if (userEmail === "default@example.com")
|
|
return `\u2705 Signed in to Khoj`;
|
|
else
|
|
return `\u2705 Signed in to Khoj as ${userEmail}`;
|
|
}
|
|
async function populateHeaderPane(headerEl, setting) {
|
|
let userInfo = null;
|
|
try {
|
|
const { userInfo: extractedUserInfo } = await canConnectToBackend(setting.khojUrl, setting.khojApiKey, false);
|
|
userInfo = extractedUserInfo;
|
|
} catch (error) {
|
|
console.error("\u2757\uFE0FCould not connect to Khoj");
|
|
}
|
|
const titleEl = headerEl.createDiv();
|
|
titleEl.className = "khoj-logo";
|
|
titleEl.textContent = "KHOJ";
|
|
const nav = headerEl.createEl("nav");
|
|
nav.className = "khoj-nav";
|
|
const chatLink = nav.createEl("a");
|
|
chatLink.id = "chat-nav";
|
|
chatLink.className = "khoj-nav chat-nav";
|
|
const chatIcon = chatLink.createEl("span");
|
|
chatIcon.className = "khoj-nav-icon khoj-nav-icon-chat";
|
|
(0, import_obsidian.setIcon)(chatIcon, "khoj-chat");
|
|
const chatText = chatLink.createEl("span");
|
|
chatText.className = "khoj-nav-item-text";
|
|
chatText.textContent = "Chat";
|
|
chatLink.appendChild(chatIcon);
|
|
chatLink.appendChild(chatText);
|
|
const searchLink = nav.createEl("a");
|
|
searchLink.id = "search-nav";
|
|
searchLink.className = "khoj-nav search-nav";
|
|
const searchIcon = searchLink.createEl("span");
|
|
searchIcon.className = "khoj-nav-icon khoj-nav-icon-search";
|
|
const searchText = searchLink.createEl("span");
|
|
searchText.className = "khoj-nav-item-text";
|
|
searchText.textContent = "Search";
|
|
searchLink.appendChild(searchIcon);
|
|
searchLink.appendChild(searchText);
|
|
const similarLink = nav.createEl("a");
|
|
similarLink.id = "similar-nav";
|
|
similarLink.className = "khoj-nav similar-nav";
|
|
const similarIcon = searchLink.createEl("span");
|
|
similarIcon.id = "similar-nav-icon";
|
|
similarIcon.className = "khoj-nav-icon khoj-nav-icon-similar";
|
|
(0, import_obsidian.setIcon)(similarIcon, "webhook");
|
|
const similarText = searchLink.createEl("span");
|
|
similarText.className = "khoj-nav-item-text";
|
|
similarText.textContent = "Similar";
|
|
similarLink.appendChild(similarIcon);
|
|
similarLink.appendChild(similarText);
|
|
nav.appendChild(chatLink);
|
|
nav.appendChild(searchLink);
|
|
nav.appendChild(similarLink);
|
|
headerEl.appendChild(titleEl);
|
|
headerEl.appendChild(nav);
|
|
}
|
|
function copyParentText(event, message, originalButton) {
|
|
var _a;
|
|
const button = event.currentTarget;
|
|
if (!button || !((_a = button == null ? void 0 : button.parentNode) == null ? void 0 : _a.textContent))
|
|
return;
|
|
if (!!button.firstChild)
|
|
button.removeChild(button.firstChild);
|
|
const textContent = message != null ? message : button.parentNode.textContent.trim();
|
|
navigator.clipboard.writeText(textContent).then(() => {
|
|
(0, import_obsidian.setIcon)(button, "copy-check");
|
|
setTimeout(() => {
|
|
(0, import_obsidian.setIcon)(button, originalButton);
|
|
}, 1e3);
|
|
}).catch((error) => {
|
|
console.error("Error copying text to clipboard:", error);
|
|
const originalButtonText = button.innerHTML;
|
|
button.innerHTML = "\u26D4\uFE0F";
|
|
setTimeout(() => {
|
|
button.innerHTML = originalButtonText;
|
|
(0, import_obsidian.setIcon)(button, originalButton);
|
|
}, 2e3);
|
|
});
|
|
return textContent;
|
|
}
|
|
function createCopyParentText(message, originalButton = "copy-plus") {
|
|
return function(event) {
|
|
return copyParentText(event, message, originalButton);
|
|
};
|
|
}
|
|
function jumpToPreviousView() {
|
|
var _a;
|
|
const editor = (_a = this.app.workspace.getActiveFileView()) == null ? void 0 : _a.editor;
|
|
if (!editor)
|
|
return;
|
|
editor.focus();
|
|
}
|
|
function pasteTextAtCursor(text) {
|
|
var _a;
|
|
const editor = (_a = this.app.workspace.getActiveFileView()) == null ? void 0 : _a.editor;
|
|
if (!editor || !text)
|
|
return;
|
|
const cursor = editor.getCursor();
|
|
if (editor == null ? void 0 : editor.getSelection()) {
|
|
editor.replaceSelection(text);
|
|
} else if (cursor) {
|
|
editor.replaceRange(text, cursor);
|
|
}
|
|
}
|
|
function getFileFromPath(sourceFiles, chosenFile) {
|
|
let fileMatch = sourceFiles.sort((a, b) => b.path.length - a.path.length).find((file) => chosenFile.replace(/\\/g, "/").endsWith(file.path));
|
|
return fileMatch;
|
|
}
|
|
function getLinkToEntry(sourceFiles, chosenFile, chosenEntry) {
|
|
let fileMatch = getFileFromPath(sourceFiles, chosenFile);
|
|
if (fileMatch) {
|
|
let resultHeading = fileMatch.extension !== "pdf" ? chosenEntry.split("\n", 1)[0] : "";
|
|
let linkToEntry = resultHeading.startsWith("#") ? `${fileMatch.path}${resultHeading}` : fileMatch.path;
|
|
console.log(`Link: ${linkToEntry}, File: ${fileMatch.path}, Heading: ${resultHeading}`);
|
|
return linkToEntry;
|
|
}
|
|
}
|
|
|
|
// src/settings.ts
|
|
var DEFAULT_SETTINGS = {
|
|
resultsCount: 6,
|
|
khojUrl: "https://app.khoj.dev",
|
|
khojApiKey: "",
|
|
connectedToBackend: false,
|
|
autoConfigure: true,
|
|
lastSync: /* @__PURE__ */ new Map(),
|
|
userInfo: null
|
|
};
|
|
var KhojSettingTab = class extends import_obsidian2.PluginSettingTab {
|
|
constructor(app, plugin) {
|
|
super(app, plugin);
|
|
this.plugin = plugin;
|
|
}
|
|
display() {
|
|
var _a;
|
|
const { containerEl } = this;
|
|
containerEl.empty();
|
|
let backendStatusEl = containerEl.createEl("small", {
|
|
text: getBackendStatusMessage(this.plugin.settings.connectedToBackend, (_a = this.plugin.settings.userInfo) == null ? void 0 : _a.email, this.plugin.settings.khojUrl, this.plugin.settings.khojApiKey)
|
|
});
|
|
let backendStatusMessage = "";
|
|
new import_obsidian2.Setting(containerEl).setName("Khoj URL").setDesc("The URL of the Khoj backend.").addText((text) => text.setValue(`${this.plugin.settings.khojUrl}`).onChange(async (value) => {
|
|
this.plugin.settings.khojUrl = value.trim().replace(/\/$/, "");
|
|
({
|
|
connectedToBackend: this.plugin.settings.connectedToBackend,
|
|
userInfo: this.plugin.settings.userInfo,
|
|
statusMessage: backendStatusMessage
|
|
} = await canConnectToBackend(this.plugin.settings.khojUrl, this.plugin.settings.khojApiKey));
|
|
await this.plugin.saveSettings();
|
|
backendStatusEl.setText(backendStatusMessage);
|
|
}));
|
|
new import_obsidian2.Setting(containerEl).setName("Khoj API Key").setDesc("Use Khoj Cloud with your Khoj API Key").addText((text) => text.setValue(`${this.plugin.settings.khojApiKey}`).onChange(async (value) => {
|
|
this.plugin.settings.khojApiKey = value.trim();
|
|
({
|
|
connectedToBackend: this.plugin.settings.connectedToBackend,
|
|
userInfo: this.plugin.settings.userInfo,
|
|
statusMessage: backendStatusMessage
|
|
} = await canConnectToBackend(this.plugin.settings.khojUrl, this.plugin.settings.khojApiKey));
|
|
await this.plugin.saveSettings();
|
|
backendStatusEl.setText(backendStatusMessage);
|
|
}));
|
|
new import_obsidian2.Setting(containerEl).setName("Results Count").setDesc("The number of results to show in search and use for chat.").addSlider((slider) => slider.setLimits(1, 10, 1).setValue(this.plugin.settings.resultsCount).setDynamicTooltip().onChange(async (value) => {
|
|
this.plugin.settings.resultsCount = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
new import_obsidian2.Setting(containerEl).setName("Auto Sync").setDesc("Automatically index your vault with Khoj.").addToggle((toggle) => toggle.setValue(this.plugin.settings.autoConfigure).onChange(async (value) => {
|
|
this.plugin.settings.autoConfigure = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
let indexVaultSetting = new import_obsidian2.Setting(containerEl);
|
|
indexVaultSetting.setName("Force Sync").setDesc("Manually force Khoj to re-index your Obsidian Vault.").addButton((button) => button.setButtonText("Update").setCta().onClick(async () => {
|
|
button.setButtonText("Updating \u{1F311}");
|
|
button.removeCta();
|
|
indexVaultSetting = indexVaultSetting.setDisabled(true);
|
|
const progress_indicator = window.setInterval(() => {
|
|
if (button.buttonEl.innerText === "Updating \u{1F311}") {
|
|
button.setButtonText("Updating \u{1F318}");
|
|
} else if (button.buttonEl.innerText === "Updating \u{1F318}") {
|
|
button.setButtonText("Updating \u{1F317}");
|
|
} else if (button.buttonEl.innerText === "Updating \u{1F317}") {
|
|
button.setButtonText("Updating \u{1F316}");
|
|
} else if (button.buttonEl.innerText === "Updating \u{1F316}") {
|
|
button.setButtonText("Updating \u{1F315}");
|
|
} else if (button.buttonEl.innerText === "Updating \u{1F315}") {
|
|
button.setButtonText("Updating \u{1F314}");
|
|
} else if (button.buttonEl.innerText === "Updating \u{1F314}") {
|
|
button.setButtonText("Updating \u{1F313}");
|
|
} else if (button.buttonEl.innerText === "Updating \u{1F313}") {
|
|
button.setButtonText("Updating \u{1F312}");
|
|
} else if (button.buttonEl.innerText === "Updating \u{1F312}") {
|
|
button.setButtonText("Updating \u{1F311}");
|
|
}
|
|
}, 300);
|
|
this.plugin.registerInterval(progress_indicator);
|
|
this.plugin.settings.lastSync = await updateContentIndex(this.app.vault, this.plugin.settings, this.plugin.settings.lastSync, true);
|
|
new import_obsidian2.Notice("\u2705 Updated Khoj index.");
|
|
window.clearInterval(progress_indicator);
|
|
button.setButtonText("Update");
|
|
button.setCta();
|
|
indexVaultSetting = indexVaultSetting.setDisabled(false);
|
|
}));
|
|
}
|
|
};
|
|
|
|
// src/search_modal.ts
|
|
var import_obsidian3 = require("obsidian");
|
|
var KhojSearchModal = class extends import_obsidian3.SuggestModal {
|
|
constructor(app, setting, find_similar_notes = false) {
|
|
super(app);
|
|
this.rerank = false;
|
|
this.query = "";
|
|
this.app = app;
|
|
this.setting = setting;
|
|
this.find_similar_notes = find_similar_notes;
|
|
this.inputEl.hidden = this.find_similar_notes;
|
|
this.scope.register(["Mod"], "Enter", async () => {
|
|
this.rerank = true;
|
|
this.inputEl.dispatchEvent(new Event("input"));
|
|
this.rerank = false;
|
|
});
|
|
this.scope.register(["Shift"], "Enter", async () => {
|
|
if (this.query != "")
|
|
createNoteAndCloseModal(this.query, this);
|
|
});
|
|
this.scope.register(["Ctrl", "Shift"], "Enter", async () => {
|
|
if (this.query != "")
|
|
createNoteAndCloseModal(this.query, this, { newLeaf: true });
|
|
});
|
|
const modalInstructions = [
|
|
{
|
|
command: "\u2191\u2193",
|
|
purpose: "to navigate"
|
|
},
|
|
{
|
|
command: "\u21B5",
|
|
purpose: "to open"
|
|
},
|
|
{
|
|
command: import_obsidian3.Platform.isMacOS ? "cmd \u21B5" : "ctrl \u21B5",
|
|
purpose: "to rerank"
|
|
},
|
|
{
|
|
command: "esc",
|
|
purpose: "to dismiss"
|
|
}
|
|
];
|
|
this.setInstructions(modalInstructions);
|
|
this.setPlaceholder("Search with Khoj...");
|
|
}
|
|
async onOpen() {
|
|
if (this.find_similar_notes) {
|
|
let file = this.app.workspace.getActiveFile();
|
|
if (file && file.extension === "md") {
|
|
this.rerank = true;
|
|
this.inputEl.value = await this.app.vault.read(file).then((file_str) => file_str.slice(0, 42110));
|
|
this.inputEl.dispatchEvent(new Event("input"));
|
|
this.rerank = false;
|
|
} else {
|
|
this.resultContainerEl.setText("Cannot find similar notes for non-markdown files");
|
|
}
|
|
}
|
|
}
|
|
async getSuggestions(query) {
|
|
let encodedQuery = encodeURIComponent(query);
|
|
let searchUrl = `${this.setting.khojUrl}/api/search?q=${encodedQuery}&n=${this.setting.resultsCount}&r=${this.rerank}&client=obsidian`;
|
|
let headers = { "Authorization": `Bearer ${this.setting.khojApiKey}` };
|
|
let response = await (0, import_obsidian3.request)({ url: `${searchUrl}`, headers });
|
|
let results = JSON.parse(response).filter((result) => {
|
|
var _a;
|
|
return !this.find_similar_notes || !result.additional.file.endsWith((_a = this.app.workspace.getActiveFile()) == null ? void 0 : _a.path);
|
|
}).map((result) => {
|
|
return { entry: result.entry, file: result.additional.file };
|
|
});
|
|
this.query = query;
|
|
return results;
|
|
}
|
|
async renderSuggestion(result, el) {
|
|
var _a;
|
|
let lines_to_render = 8;
|
|
let os_path_separator = result.file.includes("\\") ? "\\" : "/";
|
|
let filename = result.file.split(os_path_separator).pop();
|
|
el.createEl("div", { cls: "khoj-result-file" }).setText(filename != null ? filename : "");
|
|
let result_el = el.createEl("div", { cls: "khoj-result-entry" });
|
|
let resultToRender = "";
|
|
let fileExtension = (_a = filename == null ? void 0 : filename.split(".").pop()) != null ? _a : "";
|
|
if (supportedImageFilesTypes.includes(fileExtension) && filename) {
|
|
let linkToEntry = filename;
|
|
let imageFiles = this.app.vault.getFiles().filter((file) => supportedImageFilesTypes.includes(fileExtension));
|
|
let fileInVault = getFileFromPath(imageFiles, result.file);
|
|
if (fileInVault)
|
|
linkToEntry = this.app.vault.getResourcePath(fileInVault);
|
|
resultToRender = `![](${linkToEntry})`;
|
|
} else {
|
|
result.entry = result.entry.replace(/---[\n\r][\s\S]*---[\n\r]/, "");
|
|
let entry_snipped_indicator = result.entry.split("\n").length > lines_to_render ? " **...**" : "";
|
|
let snipped_entry = result.entry.split("\n").slice(0, lines_to_render).join("\n");
|
|
resultToRender = `${snipped_entry}${entry_snipped_indicator}`;
|
|
}
|
|
import_obsidian3.MarkdownRenderer.renderMarkdown(resultToRender, result_el, result.file, null);
|
|
}
|
|
async onChooseSuggestion(result, _) {
|
|
const mdFiles = this.app.vault.getMarkdownFiles();
|
|
const binaryFiles = this.app.vault.getFiles().filter((file) => supportedBinaryFileTypes.includes(file.extension));
|
|
let linkToEntry = getLinkToEntry(mdFiles.concat(binaryFiles), result.file, result.entry);
|
|
if (linkToEntry)
|
|
this.app.workspace.openLinkText(linkToEntry, "");
|
|
}
|
|
};
|
|
|
|
// src/chat_view.ts
|
|
var import_obsidian5 = require("obsidian");
|
|
var DOMPurify = __toESM(require_purify());
|
|
|
|
// src/pane_view.ts
|
|
var import_obsidian4 = require("obsidian");
|
|
var KhojPaneView = class extends import_obsidian4.ItemView {
|
|
constructor(leaf, setting) {
|
|
super(leaf);
|
|
this.setting = setting;
|
|
}
|
|
async onOpen() {
|
|
var _a, _b, _c, _d, _e;
|
|
let { contentEl } = this;
|
|
let headerEl = contentEl.createDiv({ attr: { id: "khoj-header", class: "khoj-header" } });
|
|
await populateHeaderPane(headerEl, this.setting);
|
|
(_a = headerEl.getElementsByClassName("chat-nav")[0]) == null ? void 0 : _a.classList.add("khoj-nav-selected");
|
|
(_b = headerEl.getElementsByClassName("chat-nav")[0]) == null ? void 0 : _b.addEventListener("click", (_) => {
|
|
this.activateView("khoj-chat-view" /* CHAT */);
|
|
});
|
|
(_c = headerEl.getElementsByClassName("search-nav")[0]) == null ? void 0 : _c.addEventListener("click", (_) => {
|
|
new KhojSearchModal(this.app, this.setting).open();
|
|
});
|
|
(_d = headerEl.getElementsByClassName("similar-nav")[0]) == null ? void 0 : _d.addEventListener("click", (_) => {
|
|
new KhojSearchModal(this.app, this.setting, true).open();
|
|
});
|
|
let similarNavSvgEl = (_e = headerEl.getElementsByClassName("khoj-nav-icon-similar")[0]) == null ? void 0 : _e.firstElementChild;
|
|
if (!!similarNavSvgEl)
|
|
similarNavSvgEl.id = "similar-nav-icon-svg";
|
|
}
|
|
async activateView(viewType) {
|
|
const { workspace } = this.app;
|
|
let leaf = null;
|
|
const leaves = workspace.getLeavesOfType(viewType);
|
|
if (leaves.length > 0) {
|
|
leaf = leaves[0];
|
|
} else {
|
|
leaf = workspace.getRightLeaf(false);
|
|
await (leaf == null ? void 0 : leaf.setViewState({ type: viewType, active: true }));
|
|
}
|
|
if (leaf) {
|
|
if (viewType === "khoj-chat-view" /* CHAT */) {
|
|
let chatInput = this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
|
if (chatInput)
|
|
chatInput.focus();
|
|
}
|
|
workspace.revealLeaf(leaf);
|
|
}
|
|
}
|
|
};
|
|
|
|
// src/chat_view.ts
|
|
var KhojChatView = class extends KhojPaneView {
|
|
constructor(leaf, setting) {
|
|
super(leaf, setting);
|
|
this.keyPressTimeout = null;
|
|
this.scope = new import_obsidian5.Scope(this.app.scope);
|
|
this.scope.register(["Ctrl"], "n", (_) => this.createNewConversation());
|
|
this.scope.register(["Ctrl"], "o", async (_) => await this.toggleChatSessions());
|
|
this.scope.register(["Ctrl"], "f", (_) => new KhojSearchModal(this.app, this.setting).open());
|
|
this.scope.register(["Ctrl"], "r", (_) => new KhojSearchModal(this.app, this.setting, true).open());
|
|
this.waitingForLocation = true;
|
|
fetch("https://ipapi.co/json").then((response) => response.json()).then((data) => {
|
|
this.location = {
|
|
region: data.region,
|
|
city: data.city,
|
|
countryName: data.country_name,
|
|
timezone: data.timezone
|
|
};
|
|
}).catch((err) => {
|
|
console.log(err);
|
|
}).finally(() => {
|
|
this.waitingForLocation = false;
|
|
});
|
|
}
|
|
getViewType() {
|
|
return "khoj-chat-view" /* CHAT */;
|
|
}
|
|
getDisplayText() {
|
|
return "Khoj Chat";
|
|
}
|
|
getIcon() {
|
|
return "message-circle";
|
|
}
|
|
async chat(isVoice = false) {
|
|
let input_el = this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
|
let user_message = input_el.value.trim();
|
|
input_el.value = "";
|
|
this.autoResize();
|
|
await this.getChatResponse(user_message, isVoice);
|
|
}
|
|
async onOpen() {
|
|
let { contentEl } = this;
|
|
contentEl.addClass("khoj-chat");
|
|
super.onOpen();
|
|
let defaultDomains = `'self' ${this.setting.khojUrl} https://*.obsidian.md https://app.khoj.dev https://assets.khoj.dev`;
|
|
const defaultSrc = `default-src ${defaultDomains};`;
|
|
const scriptSrc = `script-src ${defaultDomains} 'unsafe-inline';`;
|
|
const connectSrc = `connect-src ${this.setting.khojUrl} wss://*.obsidian.md/ https://ipapi.co/json;`;
|
|
const styleSrc = `style-src ${defaultDomains} 'unsafe-inline';`;
|
|
const imgSrc = `img-src * app: data:;`;
|
|
const childSrc = `child-src 'none';`;
|
|
const objectSrc = `object-src 'none';`;
|
|
const csp = `${defaultSrc} ${scriptSrc} ${connectSrc} ${styleSrc} ${imgSrc} ${childSrc} ${objectSrc}`;
|
|
let chatBodyEl = contentEl.createDiv({ attr: { id: "khoj-chat-body", class: "khoj-chat-body" } });
|
|
let inputRow = contentEl.createDiv("khoj-input-row");
|
|
let chatSessions = inputRow.createEl("button", {
|
|
text: "Chat Sessions",
|
|
attr: {
|
|
class: "khoj-input-row-button clickable-icon",
|
|
title: "Show Conversations (^O)"
|
|
}
|
|
});
|
|
chatSessions.addEventListener("click", async (_) => {
|
|
await this.toggleChatSessions();
|
|
});
|
|
(0, import_obsidian5.setIcon)(chatSessions, "history");
|
|
let chatInput = inputRow.createEl("textarea", {
|
|
attr: {
|
|
id: "khoj-chat-input",
|
|
autofocus: "autofocus",
|
|
class: "khoj-chat-input option"
|
|
}
|
|
});
|
|
chatInput.addEventListener("input", (_) => {
|
|
this.onChatInput();
|
|
});
|
|
chatInput.addEventListener("keydown", (event) => {
|
|
this.incrementalChat(event);
|
|
});
|
|
this.contentEl.addEventListener("keydown", this.handleKeyDown.bind(this));
|
|
this.contentEl.addEventListener("keyup", this.handleKeyUp.bind(this));
|
|
let transcribe = inputRow.createEl("button", {
|
|
text: "Transcribe",
|
|
attr: {
|
|
id: "khoj-transcribe",
|
|
class: "khoj-transcribe khoj-input-row-button clickable-icon ",
|
|
title: "Start Voice Chat (^S)"
|
|
}
|
|
});
|
|
transcribe.addEventListener("mousedown", (event) => {
|
|
this.startSpeechToText(event);
|
|
});
|
|
transcribe.addEventListener("mouseup", async (event) => {
|
|
await this.stopSpeechToText(event);
|
|
});
|
|
transcribe.addEventListener("touchstart", async (event) => {
|
|
await this.speechToText(event);
|
|
});
|
|
transcribe.addEventListener("touchend", async (event) => {
|
|
await this.speechToText(event);
|
|
});
|
|
transcribe.addEventListener("touchcancel", async (event) => {
|
|
await this.speechToText(event);
|
|
});
|
|
(0, import_obsidian5.setIcon)(transcribe, "mic");
|
|
let send = inputRow.createEl("button", {
|
|
text: "Send",
|
|
attr: {
|
|
id: "khoj-chat-send",
|
|
class: "khoj-chat-send khoj-input-row-button clickable-icon"
|
|
}
|
|
});
|
|
(0, import_obsidian5.setIcon)(send, "arrow-up-circle");
|
|
let sendImg = send.getElementsByClassName("lucide-arrow-up-circle")[0];
|
|
sendImg.addEventListener("click", async (_) => {
|
|
await this.chat();
|
|
});
|
|
let getChatHistorySucessfully = await this.getChatHistory(chatBodyEl);
|
|
let placeholderText = getChatHistorySucessfully ? "Message" : "Configure Khoj to enable chat";
|
|
chatInput.placeholder = placeholderText;
|
|
chatInput.disabled = !getChatHistorySucessfully;
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
this.scrollChatToBottom();
|
|
const chatInput2 = this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
|
chatInput2 == null ? void 0 : chatInput2.focus();
|
|
});
|
|
});
|
|
}
|
|
startSpeechToText(event, timeout = 200) {
|
|
if (!this.keyPressTimeout) {
|
|
this.keyPressTimeout = setTimeout(async () => {
|
|
if (this.sendMessageTimeout) {
|
|
clearTimeout(this.sendMessageTimeout);
|
|
const sendButton = this.contentEl.getElementsByClassName("khoj-chat-send")[0];
|
|
(0, import_obsidian5.setIcon)(sendButton, "arrow-up-circle");
|
|
let sendImg = sendButton.getElementsByClassName("lucide-arrow-up-circle")[0];
|
|
sendImg.addEventListener("click", async (_) => {
|
|
await this.chat();
|
|
});
|
|
const chatInput = this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
|
chatInput.value = "";
|
|
}
|
|
await this.speechToText(event);
|
|
}, timeout);
|
|
}
|
|
}
|
|
async stopSpeechToText(event) {
|
|
if (this.mediaRecorder) {
|
|
await this.speechToText(event);
|
|
}
|
|
if (this.keyPressTimeout) {
|
|
clearTimeout(this.keyPressTimeout);
|
|
this.keyPressTimeout = null;
|
|
}
|
|
}
|
|
handleKeyDown(event) {
|
|
if (event.key === "s" && event.getModifierState("Control"))
|
|
this.startSpeechToText(event);
|
|
}
|
|
async handleKeyUp(event) {
|
|
if (event.key === "s" && event.getModifierState("Control"))
|
|
await this.stopSpeechToText(event);
|
|
}
|
|
processOnlineReferences(referenceSection, onlineContext) {
|
|
let numOnlineReferences = 0;
|
|
for (let subquery in onlineContext) {
|
|
let onlineReference = onlineContext[subquery];
|
|
if (onlineReference.organic && onlineReference.organic.length > 0) {
|
|
numOnlineReferences += onlineReference.organic.length;
|
|
for (let key in onlineReference.organic) {
|
|
let reference = onlineReference.organic[key];
|
|
let polishedReference = this.generateOnlineReference(referenceSection, reference, key);
|
|
referenceSection.appendChild(polishedReference);
|
|
}
|
|
}
|
|
if (onlineReference.knowledgeGraph && onlineReference.knowledgeGraph.length > 0) {
|
|
numOnlineReferences += onlineReference.knowledgeGraph.length;
|
|
for (let key in onlineReference.knowledgeGraph) {
|
|
let reference = onlineReference.knowledgeGraph[key];
|
|
let polishedReference = this.generateOnlineReference(referenceSection, reference, key);
|
|
referenceSection.appendChild(polishedReference);
|
|
}
|
|
}
|
|
if (onlineReference.peopleAlsoAsk && onlineReference.peopleAlsoAsk.length > 0) {
|
|
numOnlineReferences += onlineReference.peopleAlsoAsk.length;
|
|
for (let key in onlineReference.peopleAlsoAsk) {
|
|
let reference = onlineReference.peopleAlsoAsk[key];
|
|
let polishedReference = this.generateOnlineReference(referenceSection, reference, key);
|
|
referenceSection.appendChild(polishedReference);
|
|
}
|
|
}
|
|
if (onlineReference.webpages && onlineReference.webpages.length > 0) {
|
|
numOnlineReferences += onlineReference.webpages.length;
|
|
for (let key in onlineReference.webpages) {
|
|
let reference = onlineReference.webpages[key];
|
|
let polishedReference = this.generateOnlineReference(referenceSection, reference, key);
|
|
referenceSection.appendChild(polishedReference);
|
|
}
|
|
}
|
|
}
|
|
return numOnlineReferences;
|
|
}
|
|
generateOnlineReference(messageEl, reference, index) {
|
|
let title = reference.title || reference.link;
|
|
let link = reference.link;
|
|
let snippet = reference.snippet;
|
|
let question = reference.question ? `<b>Question:</b> ${reference.question}<br><br>` : "";
|
|
let referenceButton = messageEl.createEl("button");
|
|
let linkElement = referenceButton.createEl("a");
|
|
linkElement.setAttribute("href", link);
|
|
linkElement.setAttribute("target", "_blank");
|
|
linkElement.setAttribute("rel", "noopener noreferrer");
|
|
linkElement.classList.add("reference-link");
|
|
linkElement.setAttribute("title", title);
|
|
linkElement.textContent = title;
|
|
referenceButton.id = `ref-${index}`;
|
|
referenceButton.classList.add("reference-button");
|
|
referenceButton.classList.add("collapsed");
|
|
referenceButton.tabIndex = 0;
|
|
referenceButton.addEventListener("click", function() {
|
|
if (this.classList.contains("collapsed")) {
|
|
this.classList.remove("collapsed");
|
|
this.classList.add("expanded");
|
|
this.innerHTML = linkElement.outerHTML + `<br><br>${question + snippet}`;
|
|
} else {
|
|
this.classList.add("collapsed");
|
|
this.classList.remove("expanded");
|
|
this.innerHTML = linkElement.outerHTML;
|
|
}
|
|
});
|
|
return referenceButton;
|
|
}
|
|
generateReference(messageEl, referenceJson, index) {
|
|
let reference = referenceJson.hasOwnProperty("compiled") ? referenceJson.compiled : referenceJson;
|
|
let referenceFile = referenceJson.hasOwnProperty("file") ? referenceJson.file : null;
|
|
const mdFiles = this.app.vault.getMarkdownFiles();
|
|
const pdfFiles = this.app.vault.getFiles().filter((file) => file.extension === "pdf");
|
|
reference = reference.split("\n").slice(1).join("\n");
|
|
let escaped_ref = reference.replace(/"/g, """);
|
|
let referenceButton = messageEl.createEl("button");
|
|
if (referenceFile) {
|
|
const linkToEntry = getLinkToEntry(mdFiles.concat(pdfFiles), referenceFile, reference);
|
|
const linkElement = referenceButton.createEl("span");
|
|
linkElement.setAttribute("title", escaped_ref);
|
|
linkElement.textContent = referenceFile;
|
|
if (linkElement && linkToEntry) {
|
|
linkElement.classList.add("reference-link");
|
|
linkElement.addEventListener("click", (event) => {
|
|
event.stopPropagation();
|
|
this.app.workspace.openLinkText(linkToEntry, "");
|
|
});
|
|
}
|
|
}
|
|
let referenceText = referenceButton.createDiv();
|
|
referenceText.textContent = escaped_ref;
|
|
referenceButton.id = `ref-${index}`;
|
|
referenceButton.classList.add("reference-button");
|
|
referenceButton.classList.add("collapsed");
|
|
referenceButton.tabIndex = 0;
|
|
referenceButton.addEventListener("click", function() {
|
|
if (this.classList.contains("collapsed")) {
|
|
this.classList.remove("collapsed");
|
|
this.classList.add("expanded");
|
|
} else {
|
|
this.classList.add("collapsed");
|
|
this.classList.remove("expanded");
|
|
}
|
|
});
|
|
return referenceButton;
|
|
}
|
|
textToSpeech(message, event = null) {
|
|
let loader = document.createElement("span");
|
|
loader.classList.add("loader");
|
|
let speechButton;
|
|
let speechIcon;
|
|
if (event === null) {
|
|
let speechButtons = document.getElementsByClassName("speech-button");
|
|
speechButton = speechButtons[speechButtons.length - 1];
|
|
let speechIcons = document.getElementsByClassName("speech-icon");
|
|
speechIcon = speechIcons[speechIcons.length - 1];
|
|
} else {
|
|
speechButton = event.currentTarget;
|
|
speechIcon = event.target;
|
|
}
|
|
speechButton.appendChild(loader);
|
|
speechButton.disabled = true;
|
|
const context = new AudioContext();
|
|
let textToSpeechApi = `${this.setting.khojUrl}/api/chat/speech?text=${encodeURIComponent(message)}`;
|
|
fetch(textToSpeechApi, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Authorization": `Bearer ${this.setting.khojApiKey}`
|
|
}
|
|
}).then((response) => response.arrayBuffer()).then((arrayBuffer) => context.decodeAudioData(arrayBuffer)).then((audioBuffer) => {
|
|
const source = context.createBufferSource();
|
|
source.buffer = audioBuffer;
|
|
source.connect(context.destination);
|
|
source.start(0);
|
|
source.onended = function() {
|
|
speechButton.removeChild(loader);
|
|
speechButton.disabled = false;
|
|
};
|
|
}).catch((err) => {
|
|
console.error("Error playing speech:", err);
|
|
speechButton.removeChild(loader);
|
|
speechButton.disabled = false;
|
|
});
|
|
}
|
|
formatHTMLMessage(message, raw = false, willReplace = true) {
|
|
message = message.replace(/<s>\[INST\].+(<\/s>)?/g, "");
|
|
message = DOMPurify.sanitize(message);
|
|
let chatMessageBodyTextEl = this.contentEl.createDiv();
|
|
chatMessageBodyTextEl.innerHTML = this.markdownTextToSanitizedHtml(message, this);
|
|
if (willReplace === true) {
|
|
this.renderActionButtons(message, chatMessageBodyTextEl);
|
|
}
|
|
return chatMessageBodyTextEl;
|
|
}
|
|
markdownTextToSanitizedHtml(markdownText, component) {
|
|
let virtualChatMessageBodyTextEl = document.createElement("div");
|
|
import_obsidian5.MarkdownRenderer.renderMarkdown(markdownText, virtualChatMessageBodyTextEl, "", component);
|
|
virtualChatMessageBodyTextEl.innerHTML = virtualChatMessageBodyTextEl.innerHTML.replace(/<img(?:(?!src=["'](app:|data:|https:\/\/generated\.khoj\.dev)).)*?>/gis, "");
|
|
return DOMPurify.sanitize(virtualChatMessageBodyTextEl.innerHTML);
|
|
}
|
|
renderMessageWithReferences(chatEl, message, sender, context, onlineContext, dt, intentType, inferredQueries) {
|
|
if (!message)
|
|
return;
|
|
let chatMessageEl;
|
|
if (intentType == null ? void 0 : intentType.includes("text-to-image")) {
|
|
let imageMarkdown = this.generateImageMarkdown(message, intentType, inferredQueries);
|
|
chatMessageEl = this.renderMessage(chatEl, imageMarkdown, sender, dt);
|
|
} else {
|
|
chatMessageEl = this.renderMessage(chatEl, message, sender, dt);
|
|
}
|
|
if ((context == null || context.length == 0) && (onlineContext == null || onlineContext && Object.keys(onlineContext).length == 0)) {
|
|
return;
|
|
}
|
|
let references = {};
|
|
if (!!context)
|
|
references["notes"] = context;
|
|
if (!!onlineContext)
|
|
references["online"] = onlineContext;
|
|
let chatMessageBodyEl = chatMessageEl.getElementsByClassName("khoj-chat-message-text")[0];
|
|
chatMessageBodyEl.appendChild(this.createReferenceSection(references));
|
|
}
|
|
generateImageMarkdown(message, intentType, inferredQueries) {
|
|
let imageMarkdown = "";
|
|
if (intentType === "text-to-image") {
|
|
imageMarkdown = `![](data:image/png;base64,${message})`;
|
|
} else if (intentType === "text-to-image2") {
|
|
imageMarkdown = `![](${message})`;
|
|
} else if (intentType === "text-to-image-v3") {
|
|
imageMarkdown = `![](data:image/webp;base64,${message})`;
|
|
}
|
|
if (inferredQueries) {
|
|
imageMarkdown += "\n\n**Inferred Query**:";
|
|
for (let inferredQuery of inferredQueries) {
|
|
imageMarkdown += `
|
|
|
|
${inferredQuery}`;
|
|
}
|
|
}
|
|
return imageMarkdown;
|
|
}
|
|
renderMessage(chatBodyEl, message, sender, dt, raw = false, willReplace = true) {
|
|
let message_time = this.formatDate(dt != null ? dt : new Date());
|
|
let emojified_sender = sender == "khoj" ? "\u{1F3EE} Khoj" : "\u{1F914} You";
|
|
let chatMessageEl = chatBodyEl.createDiv({
|
|
attr: {
|
|
"data-meta": `${emojified_sender} at ${message_time}`,
|
|
class: `khoj-chat-message ${sender}`
|
|
}
|
|
});
|
|
let chatMessageBodyEl = chatMessageEl.createDiv();
|
|
chatMessageBodyEl.addClasses(["khoj-chat-message-text", sender]);
|
|
let chatMessageBodyTextEl = chatMessageBodyEl.createDiv();
|
|
message = DOMPurify.sanitize(message);
|
|
if (raw) {
|
|
chatMessageBodyTextEl.innerHTML = message;
|
|
} else {
|
|
chatMessageBodyTextEl.innerHTML = this.markdownTextToSanitizedHtml(message, this);
|
|
}
|
|
if (willReplace === true) {
|
|
this.renderActionButtons(message, chatMessageBodyTextEl);
|
|
}
|
|
chatMessageEl.style.userSelect = "text";
|
|
this.scrollChatToBottom();
|
|
return chatMessageEl;
|
|
}
|
|
createKhojResponseDiv(dt) {
|
|
let messageTime = this.formatDate(dt != null ? dt : new Date());
|
|
let chatBodyEl = this.contentEl.getElementsByClassName("khoj-chat-body")[0];
|
|
let chatMessageEl = chatBodyEl.createDiv({
|
|
attr: {
|
|
"data-meta": `\u{1F3EE} Khoj at ${messageTime}`,
|
|
class: `khoj-chat-message khoj`
|
|
}
|
|
});
|
|
this.scrollChatToBottom();
|
|
return chatMessageEl;
|
|
}
|
|
async renderIncrementalMessage(htmlElement, additionalMessage) {
|
|
this.chatMessageState.rawResponse += additionalMessage;
|
|
htmlElement.innerHTML = "";
|
|
this.chatMessageState.rawResponse = DOMPurify.sanitize(this.chatMessageState.rawResponse);
|
|
htmlElement.innerHTML = this.markdownTextToSanitizedHtml(this.chatMessageState.rawResponse, this);
|
|
this.renderActionButtons(this.chatMessageState.rawResponse, htmlElement);
|
|
this.scrollChatToBottom();
|
|
}
|
|
renderActionButtons(message, chatMessageBodyTextEl) {
|
|
var _a;
|
|
let copyButton = this.contentEl.createEl("button");
|
|
copyButton.classList.add("chat-action-button");
|
|
copyButton.title = "Copy Message to Clipboard";
|
|
(0, import_obsidian5.setIcon)(copyButton, "copy-plus");
|
|
copyButton.addEventListener("click", createCopyParentText(message));
|
|
let pasteToFile = this.contentEl.createEl("button");
|
|
pasteToFile.classList.add("chat-action-button");
|
|
pasteToFile.title = "Paste Message to File";
|
|
(0, import_obsidian5.setIcon)(pasteToFile, "clipboard-paste");
|
|
pasteToFile.addEventListener("click", (event) => {
|
|
pasteTextAtCursor(createCopyParentText(message, "clipboard-paste")(event));
|
|
});
|
|
let speechButton = null;
|
|
if ((_a = this.setting.userInfo) == null ? void 0 : _a.is_active) {
|
|
speechButton = this.contentEl.createEl("button");
|
|
speechButton.classList.add("chat-action-button", "speech-button");
|
|
speechButton.title = "Listen to Message";
|
|
(0, import_obsidian5.setIcon)(speechButton, "speech");
|
|
speechButton.addEventListener("click", (event) => this.textToSpeech(message, event));
|
|
}
|
|
chatMessageBodyTextEl.append(copyButton, pasteToFile);
|
|
if (speechButton) {
|
|
chatMessageBodyTextEl.append(speechButton);
|
|
}
|
|
}
|
|
formatDate(date) {
|
|
let time_string = date.toLocaleTimeString("en-IN", { hour: "2-digit", minute: "2-digit", hour12: false });
|
|
let date_string = date.toLocaleString("en-IN", { year: "numeric", month: "short", day: "2-digit" }).replace(/-/g, " ");
|
|
return `${time_string}, ${date_string}`;
|
|
}
|
|
createNewConversation() {
|
|
let chatBodyEl = this.contentEl.getElementsByClassName("khoj-chat-body")[0];
|
|
chatBodyEl.innerHTML = "";
|
|
chatBodyEl.dataset.conversationId = "";
|
|
chatBodyEl.dataset.conversationTitle = "";
|
|
this.renderMessage(chatBodyEl, "Hey \u{1F44B}\u{1F3FE}, what's up?", "khoj");
|
|
}
|
|
async toggleChatSessions(forceShow = false) {
|
|
var _a;
|
|
let chatBodyEl = this.contentEl.getElementsByClassName("khoj-chat-body")[0];
|
|
if (!forceShow && ((_a = this.contentEl.getElementsByClassName("side-panel")) == null ? void 0 : _a.length) > 0) {
|
|
chatBodyEl.innerHTML = "";
|
|
return this.getChatHistory(chatBodyEl);
|
|
}
|
|
chatBodyEl.innerHTML = "";
|
|
const sidePanelEl = this.contentEl.createDiv("side-panel");
|
|
const newConversationEl = sidePanelEl.createDiv("new-conversation");
|
|
const conversationHeaderTitleEl = newConversationEl.createDiv("conversation-header-title");
|
|
conversationHeaderTitleEl.textContent = "Conversations";
|
|
const newConversationButtonEl = newConversationEl.createEl("button");
|
|
newConversationButtonEl.classList.add("new-conversation-button");
|
|
newConversationButtonEl.classList.add("side-panel-button");
|
|
newConversationButtonEl.addEventListener("click", (_) => this.createNewConversation());
|
|
(0, import_obsidian5.setIcon)(newConversationButtonEl, "plus");
|
|
newConversationButtonEl.innerHTML += "New";
|
|
newConversationButtonEl.title = "New Conversation (^N)";
|
|
const existingConversationsEl = sidePanelEl.createDiv("existing-conversations");
|
|
const conversationListEl = existingConversationsEl.createDiv("conversation-list");
|
|
const conversationListBodyHeaderEl = conversationListEl.createDiv("conversation-list-header");
|
|
const conversationListBodyEl = conversationListEl.createDiv("conversation-list-body");
|
|
const chatSessionsUrl = `${this.setting.khojUrl}/api/chat/sessions?client=obsidian`;
|
|
const headers = { "Authorization": `Bearer ${this.setting.khojApiKey}` };
|
|
try {
|
|
let response = await fetch(chatSessionsUrl, { method: "GET", headers });
|
|
let responseJson = await response.json();
|
|
let conversationId = chatBodyEl.dataset.conversationId;
|
|
if (responseJson.length > 0) {
|
|
conversationListBodyHeaderEl.style.display = "block";
|
|
for (let key in responseJson) {
|
|
let conversation = responseJson[key];
|
|
let conversationSessionEl = this.contentEl.createEl("div");
|
|
let incomingConversationId = conversation["conversation_id"];
|
|
conversationSessionEl.classList.add("conversation-session");
|
|
if (incomingConversationId == conversationId) {
|
|
conversationSessionEl.classList.add("selected-conversation");
|
|
}
|
|
const conversationTitle = conversation["slug"] || `New conversation \u{1F331}`;
|
|
const conversationSessionTitleEl = conversationSessionEl.createDiv("conversation-session-title");
|
|
conversationSessionTitleEl.textContent = conversationTitle;
|
|
conversationSessionTitleEl.addEventListener("click", () => {
|
|
chatBodyEl.innerHTML = "";
|
|
chatBodyEl.dataset.conversationId = incomingConversationId;
|
|
chatBodyEl.dataset.conversationTitle = conversationTitle;
|
|
this.getChatHistory(chatBodyEl);
|
|
});
|
|
let conversationMenuEl = this.contentEl.createEl("div");
|
|
conversationMenuEl = this.addConversationMenu(conversationMenuEl, conversationSessionEl, conversationTitle, conversationSessionTitleEl, chatBodyEl, incomingConversationId, incomingConversationId == conversationId);
|
|
conversationSessionEl.appendChild(conversationMenuEl);
|
|
conversationListBodyEl.appendChild(conversationSessionEl);
|
|
chatBodyEl.appendChild(sidePanelEl);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
addConversationMenu(conversationMenuEl, conversationSessionEl, conversationTitle, conversationSessionTitleEl, chatBodyEl, incomingConversationId, selectedConversation) {
|
|
conversationMenuEl.classList.add("conversation-menu");
|
|
const headers = { "Authorization": `Bearer ${this.setting.khojApiKey}` };
|
|
let editConversationTitleButtonEl = this.contentEl.createEl("button");
|
|
(0, import_obsidian5.setIcon)(editConversationTitleButtonEl, "edit");
|
|
editConversationTitleButtonEl.title = "Rename";
|
|
editConversationTitleButtonEl.classList.add("edit-title-button", "three-dot-menu-button-item", "clickable-icon");
|
|
if (selectedConversation)
|
|
editConversationTitleButtonEl.classList.add("selected-conversation");
|
|
editConversationTitleButtonEl.addEventListener("click", (event) => {
|
|
event.stopPropagation();
|
|
let conversationMenuChildren = conversationMenuEl.children;
|
|
let totalItems = conversationMenuChildren.length;
|
|
for (let i = totalItems - 1; i >= 0; i--) {
|
|
conversationMenuChildren[i].remove();
|
|
}
|
|
let editConversationTitleInputEl = this.contentEl.createEl("input");
|
|
editConversationTitleInputEl.classList.add("conversation-title-input");
|
|
editConversationTitleInputEl.value = conversationTitle;
|
|
editConversationTitleInputEl.addEventListener("click", function(event2) {
|
|
event2.stopPropagation();
|
|
});
|
|
editConversationTitleInputEl.addEventListener("keydown", function(event2) {
|
|
if (event2.key === "Enter") {
|
|
event2.preventDefault();
|
|
editConversationTitleSaveButtonEl.click();
|
|
}
|
|
});
|
|
let editConversationTitleSaveButtonEl = this.contentEl.createEl("button");
|
|
conversationSessionTitleEl.replaceWith(editConversationTitleInputEl);
|
|
editConversationTitleSaveButtonEl.innerHTML = "Save";
|
|
editConversationTitleSaveButtonEl.classList.add("three-dot-menu-button-item", "clickable-icon");
|
|
if (selectedConversation)
|
|
editConversationTitleSaveButtonEl.classList.add("selected-conversation");
|
|
editConversationTitleSaveButtonEl.addEventListener("click", (event2) => {
|
|
event2.stopPropagation();
|
|
let newTitle = editConversationTitleInputEl.value;
|
|
if (newTitle != null) {
|
|
let editURL = `/api/chat/title?client=web&conversation_id=${incomingConversationId}&title=${newTitle}`;
|
|
fetch(`${this.setting.khojUrl}${editURL}`, { method: "PATCH", headers }).then((response) => response.ok ? response.json() : Promise.reject(response)).then((data) => {
|
|
conversationSessionTitleEl2.textContent = newTitle;
|
|
}).catch((err) => {
|
|
return;
|
|
});
|
|
const conversationSessionTitleEl2 = conversationSessionEl.createDiv("conversation-session-title");
|
|
conversationSessionTitleEl2.textContent = newTitle;
|
|
conversationSessionTitleEl2.addEventListener("click", () => {
|
|
chatBodyEl.innerHTML = "";
|
|
chatBodyEl.dataset.conversationId = incomingConversationId;
|
|
chatBodyEl.dataset.conversationTitle = conversationTitle;
|
|
this.getChatHistory(chatBodyEl);
|
|
});
|
|
let newConversationMenuEl = this.contentEl.createEl("div");
|
|
newConversationMenuEl = this.addConversationMenu(newConversationMenuEl, conversationSessionEl, newTitle, conversationSessionTitleEl2, chatBodyEl, incomingConversationId, selectedConversation);
|
|
conversationMenuEl.replaceWith(newConversationMenuEl);
|
|
editConversationTitleInputEl.replaceWith(conversationSessionTitleEl2);
|
|
}
|
|
});
|
|
conversationMenuEl.appendChild(editConversationTitleSaveButtonEl);
|
|
});
|
|
conversationMenuEl.appendChild(editConversationTitleButtonEl);
|
|
let deleteConversationButtonEl = this.contentEl.createEl("button");
|
|
(0, import_obsidian5.setIcon)(deleteConversationButtonEl, "trash");
|
|
deleteConversationButtonEl.title = "Delete";
|
|
deleteConversationButtonEl.classList.add("delete-conversation-button", "three-dot-menu-button-item", "clickable-icon");
|
|
if (selectedConversation)
|
|
deleteConversationButtonEl.classList.add("selected-conversation");
|
|
deleteConversationButtonEl.addEventListener("click", () => {
|
|
let confirmation = confirm("Are you sure you want to delete this chat session?");
|
|
if (!confirmation)
|
|
return;
|
|
let deleteURL = `/api/chat/history?client=obsidian&conversation_id=${incomingConversationId}`;
|
|
fetch(`${this.setting.khojUrl}${deleteURL}`, { method: "DELETE", headers }).then((response) => response.ok ? response.json() : Promise.reject(response)).then((data) => {
|
|
chatBodyEl.innerHTML = "";
|
|
chatBodyEl.dataset.conversationId = "";
|
|
chatBodyEl.dataset.conversationTitle = "";
|
|
this.toggleChatSessions(true);
|
|
}).catch((err) => {
|
|
return;
|
|
});
|
|
});
|
|
conversationMenuEl.appendChild(deleteConversationButtonEl);
|
|
return conversationMenuEl;
|
|
}
|
|
async getChatHistory(chatBodyEl) {
|
|
var _a, _b;
|
|
let chatUrl = `${this.setting.khojUrl}/api/chat/history?client=obsidian`;
|
|
if (chatBodyEl.dataset.conversationId) {
|
|
chatUrl += `&conversation_id=${chatBodyEl.dataset.conversationId}`;
|
|
}
|
|
try {
|
|
let response = await fetch(chatUrl, {
|
|
method: "GET",
|
|
headers: { "Authorization": `Bearer ${this.setting.khojApiKey}` }
|
|
});
|
|
let responseJson = await response.json();
|
|
chatBodyEl.dataset.conversationId = responseJson.conversation_id;
|
|
if (responseJson.detail) {
|
|
let setupMsg = "Hi \u{1F44B}\u{1F3FE}, to start chatting add available chat models options via [the Django Admin panel](/server/admin) on the Server";
|
|
this.renderMessage(chatBodyEl, setupMsg, "khoj", void 0);
|
|
return false;
|
|
} else if (responseJson.response) {
|
|
chatBodyEl.dataset.conversationId = responseJson.response.conversation_id;
|
|
chatBodyEl.dataset.conversationTitle = responseJson.response.slug || `New conversation \u{1F331}`;
|
|
let chatLogs = ((_a = responseJson.response) == null ? void 0 : _a.conversation_id) ? (_b = responseJson.response.chat) != null ? _b : [] : responseJson.response;
|
|
chatLogs.forEach((chatLog) => {
|
|
var _a2, _b2;
|
|
this.renderMessageWithReferences(chatBodyEl, chatLog.message, chatLog.by, chatLog.context, chatLog.onlineContext, new Date(chatLog.created), (_a2 = chatLog.intent) == null ? void 0 : _a2.type, (_b2 = chatLog.intent) == null ? void 0 : _b2["inferred-queries"]);
|
|
});
|
|
}
|
|
} catch (err) {
|
|
let errorMsg = "Unable to get response from Khoj server \u2764\uFE0F\u200D\u{1FA79}. Ensure server is running or contact developers for help at [team@khoj.dev](mailto:team@khoj.dev) or in [Discord](https://discord.gg/BDgyabRM6e)";
|
|
this.renderMessage(chatBodyEl, errorMsg, "khoj", void 0);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
convertMessageChunkToJson(rawChunk) {
|
|
if ((rawChunk == null ? void 0 : rawChunk.startsWith("{")) && (rawChunk == null ? void 0 : rawChunk.endsWith("}"))) {
|
|
try {
|
|
let jsonChunk = JSON.parse(rawChunk);
|
|
if (!jsonChunk.type)
|
|
jsonChunk = { type: "message", data: jsonChunk };
|
|
return jsonChunk;
|
|
} catch (e) {
|
|
return { type: "message", data: rawChunk };
|
|
}
|
|
} else if (rawChunk.length > 0) {
|
|
return { type: "message", data: rawChunk };
|
|
}
|
|
return { type: "", data: "" };
|
|
}
|
|
processMessageChunk(rawChunk) {
|
|
var _a, _b, _c;
|
|
const chunk = this.convertMessageChunkToJson(rawChunk);
|
|
console.debug("Chunk:", chunk);
|
|
if (!chunk || !chunk.type)
|
|
return;
|
|
if (chunk.type === "status") {
|
|
console.log(`status: ${chunk.data}`);
|
|
const statusMessage = chunk.data;
|
|
this.handleStreamResponse(this.chatMessageState.newResponseTextEl, statusMessage, this.chatMessageState.loadingEllipsis, false);
|
|
} else if (chunk.type === "start_llm_response") {
|
|
console.log("Started streaming", new Date());
|
|
} else if (chunk.type === "end_llm_response") {
|
|
console.log("Stopped streaming", new Date());
|
|
if (this.chatMessageState.isVoice && ((_a = this.setting.userInfo) == null ? void 0 : _a.is_active))
|
|
this.textToSpeech(this.chatMessageState.rawResponse);
|
|
this.finalizeChatBodyResponse(this.chatMessageState.references, this.chatMessageState.newResponseTextEl);
|
|
const liveQuery = this.chatMessageState.rawQuery;
|
|
this.chatMessageState = {
|
|
newResponseTextEl: null,
|
|
newResponseEl: null,
|
|
loadingEllipsis: null,
|
|
references: {},
|
|
rawResponse: "",
|
|
rawQuery: liveQuery,
|
|
isVoice: false
|
|
};
|
|
} else if (chunk.type === "references") {
|
|
this.chatMessageState.references = { "notes": chunk.data.context, "online": chunk.data.onlineContext };
|
|
} else if (chunk.type === "message") {
|
|
const chunkData = chunk.data;
|
|
if (typeof chunkData === "object" && chunkData !== null) {
|
|
this.handleJsonResponse(chunkData);
|
|
} else if (typeof chunkData === "string" && ((_b = chunkData.trim()) == null ? void 0 : _b.startsWith("{")) && ((_c = chunkData.trim()) == null ? void 0 : _c.endsWith("}"))) {
|
|
try {
|
|
const jsonData = JSON.parse(chunkData.trim());
|
|
this.handleJsonResponse(jsonData);
|
|
} catch (e) {
|
|
this.chatMessageState.rawResponse += chunkData;
|
|
this.handleStreamResponse(this.chatMessageState.newResponseTextEl, this.chatMessageState.rawResponse, this.chatMessageState.loadingEllipsis);
|
|
}
|
|
} else {
|
|
this.chatMessageState.rawResponse += chunkData;
|
|
this.handleStreamResponse(this.chatMessageState.newResponseTextEl, this.chatMessageState.rawResponse, this.chatMessageState.loadingEllipsis);
|
|
}
|
|
}
|
|
}
|
|
handleJsonResponse(jsonData) {
|
|
if (jsonData.image || jsonData.detail) {
|
|
this.chatMessageState.rawResponse = this.handleImageResponse(jsonData, this.chatMessageState.rawResponse);
|
|
} else if (jsonData.response) {
|
|
this.chatMessageState.rawResponse = jsonData.response;
|
|
}
|
|
if (this.chatMessageState.newResponseTextEl) {
|
|
this.chatMessageState.newResponseTextEl.innerHTML = "";
|
|
this.chatMessageState.newResponseTextEl.appendChild(this.formatHTMLMessage(this.chatMessageState.rawResponse));
|
|
}
|
|
}
|
|
async readChatStream(response) {
|
|
if (response.body == null)
|
|
return;
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
const eventDelimiter = "\u2403\u{1F51A}\u2417";
|
|
let buffer = "";
|
|
while (true) {
|
|
const { value, done } = await reader.read();
|
|
if (done) {
|
|
this.processMessageChunk(buffer);
|
|
buffer = "";
|
|
break;
|
|
}
|
|
const chunk = decoder.decode(value, { stream: true });
|
|
console.debug("Raw Chunk:", chunk);
|
|
buffer += chunk;
|
|
let newEventIndex;
|
|
while ((newEventIndex = buffer.indexOf(eventDelimiter)) !== -1) {
|
|
const event = buffer.slice(0, newEventIndex);
|
|
buffer = buffer.slice(newEventIndex + eventDelimiter.length);
|
|
if (event)
|
|
this.processMessageChunk(event);
|
|
}
|
|
}
|
|
}
|
|
async getChatResponse(query, isVoice = false) {
|
|
if (!query || query === "")
|
|
return;
|
|
let chatBodyEl = this.contentEl.getElementsByClassName("khoj-chat-body")[0];
|
|
this.renderMessage(chatBodyEl, query, "you");
|
|
let conversationId = chatBodyEl.dataset.conversationId;
|
|
if (!conversationId) {
|
|
let chatUrl2 = `${this.setting.khojUrl}/api/chat/sessions?client=obsidian`;
|
|
let response2 = await fetch(chatUrl2, {
|
|
method: "POST",
|
|
headers: { "Authorization": `Bearer ${this.setting.khojApiKey}` }
|
|
});
|
|
let data = await response2.json();
|
|
conversationId = data.conversation_id;
|
|
chatBodyEl.dataset.conversationId = conversationId;
|
|
}
|
|
let encodedQuery = encodeURIComponent(query);
|
|
let chatUrl = `${this.setting.khojUrl}/api/chat?q=${encodedQuery}&conversation_id=${conversationId}&n=${this.setting.resultsCount}&stream=true&client=obsidian`;
|
|
if (!!this.location)
|
|
chatUrl += `®ion=${this.location.region}&city=${this.location.city}&country=${this.location.countryName}&timezone=${this.location.timezone}`;
|
|
let newResponseEl = this.createKhojResponseDiv();
|
|
let newResponseTextEl = newResponseEl.createDiv();
|
|
newResponseTextEl.classList.add("khoj-chat-message-text", "khoj");
|
|
let loadingEllipsis = this.createLoadingEllipse();
|
|
newResponseTextEl.appendChild(loadingEllipsis);
|
|
this.chatMessageState = {
|
|
newResponseEl,
|
|
newResponseTextEl,
|
|
loadingEllipsis,
|
|
references: {},
|
|
rawQuery: query,
|
|
rawResponse: "",
|
|
isVoice
|
|
};
|
|
let response = await fetch(chatUrl, {
|
|
method: "GET",
|
|
headers: {
|
|
"Content-Type": "text/plain",
|
|
"Authorization": `Bearer ${this.setting.khojApiKey}`
|
|
}
|
|
});
|
|
try {
|
|
if (response.body === null)
|
|
throw new Error("Response body is null");
|
|
await this.readChatStream(response);
|
|
} catch (err) {
|
|
console.error(`Khoj chat response failed with
|
|
${err}`);
|
|
let errorMsg = "Sorry, unable to get response from Khoj backend \u2764\uFE0F\u200D\u{1FA79}. Retry or contact developers for help at <a href=mailto:'team@khoj.dev'>team@khoj.dev</a> or <a href='https://discord.gg/BDgyabRM6e'>on Discord</a>";
|
|
newResponseTextEl.textContent = errorMsg;
|
|
}
|
|
}
|
|
flashStatusInChatInput(message) {
|
|
let chatInput = this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
|
let originalPlaceholder = chatInput.placeholder;
|
|
chatInput.placeholder = message;
|
|
setTimeout(() => {
|
|
chatInput.placeholder = originalPlaceholder;
|
|
}, 2e3);
|
|
}
|
|
async clearConversationHistory() {
|
|
let chatBody = this.contentEl.getElementsByClassName("khoj-chat-body")[0];
|
|
let response = await (0, import_obsidian5.request)({
|
|
url: `${this.setting.khojUrl}/api/chat/history?client=obsidian`,
|
|
method: "DELETE",
|
|
headers: { "Authorization": `Bearer ${this.setting.khojApiKey}` }
|
|
});
|
|
try {
|
|
let result = JSON.parse(response);
|
|
if (result.status !== "ok") {
|
|
throw new Error("Failed to clear conversation history");
|
|
} else {
|
|
let getChatHistoryStatus = await this.getChatHistory(chatBody);
|
|
if (getChatHistoryStatus)
|
|
chatBody.innerHTML = "";
|
|
let statusMsg = getChatHistoryStatus ? result.message : "Failed to clear conversation history";
|
|
this.flashStatusInChatInput(statusMsg);
|
|
}
|
|
} catch (err) {
|
|
this.flashStatusInChatInput("Failed to clear conversation history");
|
|
}
|
|
}
|
|
async speechToText(event) {
|
|
var _a, _b;
|
|
event.preventDefault();
|
|
const transcribeButton = this.contentEl.getElementsByClassName("khoj-transcribe")[0];
|
|
const chatInput = this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
|
const sendButton = this.contentEl.getElementsByClassName("khoj-chat-send")[0];
|
|
const generateRequestBody = async (audioBlob, boundary_string) => {
|
|
const boundary = `------${boundary_string}`;
|
|
const chunks = [];
|
|
chunks.push(new TextEncoder().encode(`${boundary}\r
|
|
`));
|
|
chunks.push(new TextEncoder().encode(`Content-Disposition: form-data; name="file"; filename="blob"\r
|
|
Content-Type: "application/octet-stream"\r
|
|
\r
|
|
`));
|
|
chunks.push(await audioBlob.arrayBuffer());
|
|
chunks.push(new TextEncoder().encode("\r\n"));
|
|
await Promise.all(chunks);
|
|
chunks.push(new TextEncoder().encode(`${boundary}--\r
|
|
`));
|
|
return await new Blob(chunks).arrayBuffer();
|
|
};
|
|
const sendToServer = async (audioBlob) => {
|
|
const boundary_string = `Boundary${Math.random().toString(36).slice(2)}`;
|
|
const requestBody = await generateRequestBody(audioBlob, boundary_string);
|
|
const response = await (0, import_obsidian5.requestUrl)({
|
|
url: `${this.setting.khojUrl}/api/transcribe?client=obsidian`,
|
|
method: "POST",
|
|
headers: { "Authorization": `Bearer ${this.setting.khojApiKey}` },
|
|
contentType: `multipart/form-data; boundary=----${boundary_string}`,
|
|
body: requestBody
|
|
});
|
|
let noSpeechText = [
|
|
"Thanks for watching!",
|
|
"Thanks for watching.",
|
|
"Thank you for watching!",
|
|
"Thank you for watching.",
|
|
"You",
|
|
"Bye."
|
|
];
|
|
let noSpeech = false;
|
|
if (response.status === 200) {
|
|
console.log(response);
|
|
noSpeech = noSpeechText.includes(response.json.text.trimStart());
|
|
if (!noSpeech)
|
|
chatInput.value += response.json.text.trimStart();
|
|
this.autoResize();
|
|
} else if (response.status === 501) {
|
|
throw new Error("\u26D4\uFE0F Configure speech-to-text model on server.");
|
|
} else if (response.status === 422) {
|
|
throw new Error("\u26D4\uFE0F Audio file to large to process.");
|
|
} else {
|
|
throw new Error("\u26D4\uFE0F Failed to transcribe audio.");
|
|
}
|
|
if (chatInput.value.length === 0 || noSpeech)
|
|
return;
|
|
(0, import_obsidian5.setIcon)(sendButton, "stop-circle");
|
|
let stopSendButtonImg = sendButton.getElementsByClassName("lucide-stop-circle")[0];
|
|
stopSendButtonImg.addEventListener("click", (_) => {
|
|
this.cancelSendMessage();
|
|
});
|
|
stopSendButtonImg.getElementsByTagName("circle")[0].style.animation = "countdown 3s linear 1 forwards";
|
|
stopSendButtonImg.getElementsByTagName("circle")[0].style.color = "var(--icon-color-active)";
|
|
this.sendMessageTimeout = setTimeout(() => {
|
|
(0, import_obsidian5.setIcon)(sendButton, "arrow-up-circle");
|
|
let sendImg = sendButton.getElementsByClassName("lucide-arrow-up-circle")[0];
|
|
sendImg.addEventListener("click", async (_) => {
|
|
await this.chat();
|
|
});
|
|
this.chat(true);
|
|
}, 3e3);
|
|
};
|
|
const handleRecording = (stream) => {
|
|
const audioChunks = [];
|
|
const recordingConfig = { mimeType: "audio/webm" };
|
|
this.mediaRecorder = new MediaRecorder(stream, recordingConfig);
|
|
this.mediaRecorder.addEventListener("dataavailable", function(event2) {
|
|
if (event2.data.size > 0)
|
|
audioChunks.push(event2.data);
|
|
});
|
|
this.mediaRecorder.addEventListener("stop", async function() {
|
|
const audioBlob = new Blob(audioChunks, { type: "audio/webm" });
|
|
await sendToServer(audioBlob);
|
|
});
|
|
this.mediaRecorder.start();
|
|
transcribeButton.classList.add("loading-encircle");
|
|
};
|
|
if (!this.mediaRecorder || this.mediaRecorder.state === "inactive" || event.type === "touchstart" || event.type === "mousedown" || event.type === "keydown") {
|
|
(_a = navigator.mediaDevices.getUserMedia({ audio: true })) == null ? void 0 : _a.then(handleRecording).catch((e) => {
|
|
this.flashStatusInChatInput("\u26D4\uFE0F Failed to access microphone");
|
|
});
|
|
} else if (((_b = this.mediaRecorder) == null ? void 0 : _b.state) === "recording" || event.type === "touchend" || event.type === "touchcancel" || event.type === "mouseup" || event.type === "keyup") {
|
|
this.mediaRecorder.stop();
|
|
this.mediaRecorder.stream.getTracks().forEach((track) => track.stop());
|
|
this.mediaRecorder = void 0;
|
|
transcribeButton.classList.remove("loading-encircle");
|
|
(0, import_obsidian5.setIcon)(transcribeButton, "mic");
|
|
}
|
|
}
|
|
cancelSendMessage() {
|
|
clearTimeout(this.sendMessageTimeout);
|
|
let sendButton = this.contentEl.getElementsByClassName("khoj-chat-send")[0];
|
|
(0, import_obsidian5.setIcon)(sendButton, "arrow-up-circle");
|
|
let sendImg = sendButton.getElementsByClassName("lucide-arrow-up-circle")[0];
|
|
sendImg.addEventListener("click", async (_) => {
|
|
await this.chat();
|
|
});
|
|
}
|
|
incrementalChat(event) {
|
|
if (!event.shiftKey && event.key === "Enter") {
|
|
event.preventDefault();
|
|
this.chat();
|
|
}
|
|
}
|
|
onChatInput() {
|
|
const chatInput = this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
|
chatInput.value = chatInput.value.trimStart();
|
|
this.autoResize();
|
|
}
|
|
autoResize() {
|
|
const chatInput = this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
|
const scrollTop = chatInput.scrollTop;
|
|
chatInput.style.height = "0";
|
|
const scrollHeight = chatInput.scrollHeight + 8;
|
|
chatInput.style.height = Math.min(scrollHeight, 200) + "px";
|
|
chatInput.scrollTop = scrollTop;
|
|
this.scrollChatToBottom();
|
|
}
|
|
scrollChatToBottom() {
|
|
const chat_body_el = this.contentEl.getElementsByClassName("khoj-chat-body")[0];
|
|
if (!!chat_body_el)
|
|
chat_body_el.scrollTop = chat_body_el.scrollHeight;
|
|
}
|
|
createLoadingEllipse() {
|
|
let loadingEllipsis = this.contentEl.createEl("div");
|
|
loadingEllipsis.classList.add("lds-ellipsis");
|
|
let firstEllipsis = this.contentEl.createEl("div");
|
|
firstEllipsis.classList.add("lds-ellipsis-item");
|
|
let secondEllipsis = this.contentEl.createEl("div");
|
|
secondEllipsis.classList.add("lds-ellipsis-item");
|
|
let thirdEllipsis = this.contentEl.createEl("div");
|
|
thirdEllipsis.classList.add("lds-ellipsis-item");
|
|
let fourthEllipsis = this.contentEl.createEl("div");
|
|
fourthEllipsis.classList.add("lds-ellipsis-item");
|
|
loadingEllipsis.appendChild(firstEllipsis);
|
|
loadingEllipsis.appendChild(secondEllipsis);
|
|
loadingEllipsis.appendChild(thirdEllipsis);
|
|
loadingEllipsis.appendChild(fourthEllipsis);
|
|
return loadingEllipsis;
|
|
}
|
|
handleStreamResponse(newResponseElement, rawResponse, loadingEllipsis, replace = true) {
|
|
if (!newResponseElement)
|
|
return;
|
|
if (newResponseElement.getElementsByClassName("lds-ellipsis").length > 0 && loadingEllipsis)
|
|
newResponseElement.removeChild(loadingEllipsis);
|
|
if (replace)
|
|
newResponseElement.innerHTML = "";
|
|
newResponseElement.appendChild(this.formatHTMLMessage(rawResponse, false, replace));
|
|
if (!replace && loadingEllipsis)
|
|
newResponseElement.appendChild(loadingEllipsis);
|
|
this.scrollChatToBottom();
|
|
}
|
|
handleImageResponse(imageJson, rawResponse) {
|
|
var _a, _b;
|
|
if (imageJson.image) {
|
|
const inferredQuery = (_b = (_a = imageJson.inferredQueries) == null ? void 0 : _a[0]) != null ? _b : "generated image";
|
|
if (imageJson.intentType === "text-to-image") {
|
|
rawResponse += `![generated_image](data:image/png;base64,${imageJson.image})`;
|
|
} else if (imageJson.intentType === "text-to-image2") {
|
|
rawResponse += `![generated_image](${imageJson.image})`;
|
|
} else if (imageJson.intentType === "text-to-image-v3") {
|
|
rawResponse = `![](data:image/webp;base64,${imageJson.image})`;
|
|
}
|
|
if (inferredQuery) {
|
|
rawResponse += `
|
|
|
|
**Inferred Query**:
|
|
|
|
${inferredQuery}`;
|
|
}
|
|
}
|
|
if (imageJson.detail)
|
|
rawResponse += imageJson.detail;
|
|
return rawResponse;
|
|
}
|
|
finalizeChatBodyResponse(references, newResponseElement) {
|
|
if (!!newResponseElement && references != null && Object.keys(references).length > 0) {
|
|
newResponseElement.appendChild(this.createReferenceSection(references));
|
|
}
|
|
this.scrollChatToBottom();
|
|
let chatInput = this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
|
if (chatInput)
|
|
chatInput.removeAttribute("disabled");
|
|
}
|
|
createReferenceSection(references) {
|
|
let referenceSection = this.contentEl.createEl("div");
|
|
referenceSection.classList.add("reference-section");
|
|
referenceSection.classList.add("collapsed");
|
|
let numReferences = 0;
|
|
if (references.hasOwnProperty("notes")) {
|
|
numReferences += references["notes"].length;
|
|
references["notes"].forEach((reference, index) => {
|
|
let polishedReference = this.generateReference(referenceSection, reference, index);
|
|
referenceSection.appendChild(polishedReference);
|
|
});
|
|
}
|
|
if (references.hasOwnProperty("online")) {
|
|
numReferences += this.processOnlineReferences(referenceSection, references["online"]);
|
|
}
|
|
let referenceExpandButton = this.contentEl.createEl("button");
|
|
referenceExpandButton.classList.add("reference-expand-button");
|
|
referenceExpandButton.innerHTML = numReferences == 1 ? "1 reference" : `${numReferences} references`;
|
|
referenceExpandButton.addEventListener("click", function() {
|
|
if (referenceSection.classList.contains("collapsed")) {
|
|
referenceSection.classList.remove("collapsed");
|
|
referenceSection.classList.add("expanded");
|
|
} else {
|
|
referenceSection.classList.add("collapsed");
|
|
referenceSection.classList.remove("expanded");
|
|
}
|
|
});
|
|
let referencesDiv = this.contentEl.createEl("div");
|
|
referencesDiv.classList.add("references");
|
|
referencesDiv.appendChild(referenceExpandButton);
|
|
referencesDiv.appendChild(referenceSection);
|
|
return referencesDiv;
|
|
}
|
|
};
|
|
|
|
// src/main.ts
|
|
var Khoj = class extends import_obsidian6.Plugin {
|
|
async onload() {
|
|
await this.loadSettings();
|
|
this.addCommand({
|
|
id: "search",
|
|
name: "Search",
|
|
callback: () => {
|
|
new KhojSearchModal(this.app, this.settings).open();
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "similar",
|
|
name: "Find similar notes",
|
|
editorCallback: () => {
|
|
new KhojSearchModal(this.app, this.settings, true).open();
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "chat",
|
|
name: "Chat",
|
|
callback: () => {
|
|
this.activateView("khoj-chat-view" /* CHAT */);
|
|
}
|
|
});
|
|
this.registerView("khoj-chat-view" /* CHAT */, (leaf) => new KhojChatView(leaf, this.settings));
|
|
this.addRibbonIcon("message-circle", "Khoj", (_) => {
|
|
this.activateView("khoj-chat-view" /* CHAT */);
|
|
});
|
|
this.addSettingTab(new KhojSettingTab(this.app, this));
|
|
this.indexingTimer = setInterval(async () => {
|
|
if (this.settings.autoConfigure) {
|
|
this.settings.lastSync = await updateContentIndex(this.app.vault, this.settings, this.settings.lastSync);
|
|
}
|
|
}, 60 * 60 * 1e3);
|
|
}
|
|
async loadSettings() {
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
|
({ connectedToBackend: this.settings.connectedToBackend } = await canConnectToBackend(this.settings.khojUrl, this.settings.khojApiKey, true));
|
|
}
|
|
async saveSettings() {
|
|
this.saveData(this.settings);
|
|
}
|
|
async onunload() {
|
|
if (this.indexingTimer)
|
|
clearInterval(this.indexingTimer);
|
|
this.unload();
|
|
}
|
|
async activateView(viewType) {
|
|
var _a;
|
|
const { workspace } = this.app;
|
|
let leaf = null;
|
|
const leaves = workspace.getLeavesOfType(viewType);
|
|
if (leaves.length > 0) {
|
|
leaf = leaves[0];
|
|
} else {
|
|
leaf = workspace.getRightLeaf(false);
|
|
await (leaf == null ? void 0 : leaf.setViewState({ type: viewType, active: true }));
|
|
}
|
|
if (leaf) {
|
|
const activeKhojLeaf = (_a = workspace.getActiveViewOfType(KhojPaneView)) == null ? void 0 : _a.leaf;
|
|
if (activeKhojLeaf === leaf)
|
|
jumpToPreviousView();
|
|
else {
|
|
workspace.revealLeaf(leaf);
|
|
if (viewType === "khoj-chat-view" /* CHAT */) {
|
|
let chatView = leaf.view;
|
|
let chatInput = chatView.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
|
if (chatInput)
|
|
chatInput.focus();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
/*! @license DOMPurify 3.1.5 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.1.5/LICENSE */
|