diff --git a/client/modules/IDE/actions/uploader.js b/client/modules/IDE/actions/uploader.js index 7cc2b6ca..8a5a3c23 100644 --- a/client/modules/IDE/actions/uploader.js +++ b/client/modules/IDE/actions/uploader.js @@ -1,7 +1,7 @@ import axios from 'axios'; import { createFile } from './files'; +import { TEXT_FILE_REGEX } from '../../../../server/utils/fileUtils'; -const textFileRegex = /(text\/|application\/json)/; const s3BucketHttps = process.env.S3_BUCKET_URL_BASE || `https://s3-${process.env.AWS_REGION}.amazonaws.com/${process.env.S3_BUCKET}/`; const ROOT_URL = process.env.API_URL; @@ -33,10 +33,8 @@ function localIntercept(file, options = {}) { export function dropzoneAcceptCallback(userId, file, done) { return () => { - // for text files and small files - // check mime type - // if text, local interceptor - if (file.type.match(textFileRegex) && file.size < MAX_LOCAL_FILE_SIZE) { + // if a user would want to edit this file as text, local interceptor + if (file.name.match(TEXT_FILE_REGEX) && file.size < MAX_LOCAL_FILE_SIZE) { localIntercept(file).then((result) => { file.content = result; // eslint-disable-line done('Uploading plaintext file locally.'); @@ -79,7 +77,7 @@ export function dropzoneAcceptCallback(userId, file, done) { export function dropzoneSendingCallback(file, xhr, formData) { return () => { - if (!file.type.match(textFileRegex) || file.size >= MAX_LOCAL_FILE_SIZE) { + if (!file.name.match(TEXT_FILE_REGEX) || file.size >= MAX_LOCAL_FILE_SIZE) { Object.keys(file.postData).forEach((key) => { formData.append(key, file.postData[key]); }); @@ -92,7 +90,7 @@ export function dropzoneSendingCallback(file, xhr, formData) { export function dropzoneCompleteCallback(file) { return (dispatch, getState) => { // eslint-disable-line - if ((!file.type.match(textFileRegex) || file.size >= MAX_LOCAL_FILE_SIZE) && file.status !== 'error') { + if ((!file.name.match(TEXT_FILE_REGEX) || file.size >= MAX_LOCAL_FILE_SIZE) && file.status !== 'error') { let inputHidden = '!?|\/]/; + + var curPunc; + + function tokenBase(stream, state) { + var ch = stream.next(); + if (hooks[ch]) { + var result = hooks[ch](stream, state); + if (result !== false) return result; + } + if (ch == '"' || ch == "'") { + state.tokenize = tokenString(ch); + return state.tokenize(stream, state); + } + if (/[\[\]{}\(\),;\:\.]/.test(ch)) { + curPunc = ch; + return null; + } + if (/\d/.test(ch)) { + stream.eatWhile(/[\w\.]/); + return "number"; + } + if (ch == "/") { + if (stream.eat("*")) { + state.tokenize = tokenComment; + return tokenComment(stream, state); + } + if (stream.eat("/")) { + stream.skipToEnd(); + return "comment"; + } + } + if (isOperatorChar.test(ch)) { + stream.eatWhile(isOperatorChar); + return "operator"; + } + stream.eatWhile(/[\w\$_\xa1-\uffff]/); + var cur = stream.current(); + if (keywords.propertyIsEnumerable(cur)) { + if (blockKeywords.propertyIsEnumerable(cur)) curPunc = "newstatement"; + return "keyword"; + } + if (builtin.propertyIsEnumerable(cur)) { + if (blockKeywords.propertyIsEnumerable(cur)) curPunc = "newstatement"; + return "builtin"; + } + if (atoms.propertyIsEnumerable(cur)) return "atom"; + return "variable"; + } + + function tokenString(quote) { + return function(stream, state) { + var escaped = false, next, end = false; + while ((next = stream.next()) != null) { + if (next == quote && !escaped) {end = true; break;} + escaped = !escaped && next == "\\"; + } + if (end || !(escaped || multiLineStrings)) + state.tokenize = null; + return "string"; + }; + } + + function tokenComment(stream, state) { + var maybeEnd = false, ch; + while (ch = stream.next()) { + if (ch == "/" && maybeEnd) { + state.tokenize = null; + break; + } + maybeEnd = (ch == "*"); + } + return "comment"; + } + + function Context(indented, column, type, align, prev) { + this.indented = indented; + this.column = column; + this.type = type; + this.align = align; + this.prev = prev; + } + function pushContext(state, col, type) { + var indent = state.indented; + if (state.context && state.context.type == "statement") + indent = state.context.indented; + return state.context = new Context(indent, col, type, null, state.context); + } + function popContext(state) { + var t = state.context.type; + if (t == ")" || t == "]" || t == "}") + state.indented = state.context.indented; + return state.context = state.context.prev; + } + + // Interface + + return { + startState: function(basecolumn) { + return { + tokenize: null, + context: new Context((basecolumn || 0) - indentUnit, 0, "top", false), + indented: 0, + startOfLine: true + }; + }, + + token: function(stream, state) { + var ctx = state.context; + if (stream.sol()) { + if (ctx.align == null) ctx.align = false; + state.indented = stream.indentation(); + state.startOfLine = true; + } + if (stream.eatSpace()) return null; + curPunc = null; + var style = (state.tokenize || tokenBase)(stream, state); + if (style == "comment" || style == "meta") return style; + if (ctx.align == null) ctx.align = true; + + if ((curPunc == ";" || curPunc == ":" || curPunc == ",") && ctx.type == "statement") popContext(state); + else if (curPunc == "{") pushContext(state, stream.column(), "}"); + else if (curPunc == "[") pushContext(state, stream.column(), "]"); + else if (curPunc == "(") pushContext(state, stream.column(), ")"); + else if (curPunc == "}") { + while (ctx.type == "statement") ctx = popContext(state); + if (ctx.type == "}") ctx = popContext(state); + while (ctx.type == "statement") ctx = popContext(state); + } + else if (curPunc == ctx.type) popContext(state); + else if (indentStatements && + (((ctx.type == "}" || ctx.type == "top") && curPunc != ';') || + (ctx.type == "statement" && curPunc == "newstatement"))) + pushContext(state, stream.column(), "statement"); + state.startOfLine = false; + return style; + }, + + indent: function(state, textAfter) { + if (state.tokenize != tokenBase && state.tokenize != null) return CodeMirror.Pass; + var ctx = state.context, firstChar = textAfter && textAfter.charAt(0); + if (ctx.type == "statement" && firstChar == "}") ctx = ctx.prev; + var closing = firstChar == ctx.type; + if (ctx.type == "statement") return ctx.indented + (firstChar == "{" ? 0 : statementIndentUnit); + else if (ctx.align && (!dontAlignCalls || ctx.type != ")")) return ctx.column + (closing ? 0 : 1); + else if (ctx.type == ")" && !closing) return ctx.indented + statementIndentUnit; + else return ctx.indented + (closing ? 0 : indentUnit); + }, + + electricChars: "{}", + blockCommentStart: "/*", + blockCommentEnd: "*/", + lineComment: "//", + fold: "brace" + }; +}); + + function words(str) { + var obj = {}, words = str.split(" "); + for (var i = 0; i < words.length; ++i) obj[words[i]] = true; + return obj; + } + var cKeywords = "auto if break int case long char register continue return default short do sizeof " + + "double static else struct entry switch extern typedef float union for unsigned " + + "goto while enum void const signed volatile"; + + function cppHook(stream, state) { + if (!state.startOfLine) return false; + for (;;) { + if (stream.skipTo("\\")) { + stream.next(); + if (stream.eol()) { + state.tokenize = cppHook; + break; + } + } else { + stream.skipToEnd(); + state.tokenize = null; + break; + } + } + return "meta"; + } + + function cpp11StringHook(stream, state) { + stream.backUp(1); + // Raw strings. + if (stream.match(/(R|u8R|uR|UR|LR)/)) { + var match = stream.match(/"([^\s\\()]{0,16})\(/); + if (!match) { + return false; + } + state.cpp11RawStringDelim = match[1]; + state.tokenize = tokenRawString; + return tokenRawString(stream, state); + } + // Unicode strings/chars. + if (stream.match(/(u8|u|U|L)/)) { + if (stream.match(/["']/, /* eat */ false)) { + return "string"; + } + return false; + } + // Ignore this hook. + stream.next(); + return false; + } + + // C#-style strings where "" escapes a quote. + function tokenAtString(stream, state) { + var next; + while ((next = stream.next()) != null) { + if (next == '"' && !stream.eat('"')) { + state.tokenize = null; + break; + } + } + return "string"; + } + + // C++11 raw string literal is "( anything )", where + // can be a string up to 16 characters long. + function tokenRawString(stream, state) { + // Escape characters that have special regex meanings. + var delim = state.cpp11RawStringDelim.replace(/[^\w\s]/g, '\\$&'); + var match = stream.match(new RegExp(".*?\\)" + delim + '"')); + if (match) + state.tokenize = null; + else + stream.skipToEnd(); + return "string"; + } + + function def(mimes, mode) { + if (typeof mimes == "string") mimes = [mimes]; + var words = []; + function add(obj) { + if (obj) for (var prop in obj) if (obj.hasOwnProperty(prop)) + words.push(prop); + } + add(mode.keywords); + add(mode.builtin); + add(mode.atoms); + if (words.length) { + mode.helperType = mimes[0]; + CodeMirror.registerHelper("hintWords", mimes[0], words); + } + + for (var i = 0; i < mimes.length; ++i) + CodeMirror.defineMIME(mimes[i], mode); + } + + def(["x-shader/x-vertex", "x-shader/x-fragment"], { + name: "clike", + keywords: words("float int bool void " + + "vec2 vec3 vec4 ivec2 ivec3 ivec4 bvec2 bvec3 bvec4 " + + "mat2 mat3 mat4 " + + "sampler2D sampler3D samplerCube " + + "const attribute uniform varying " + + "break continue discard return " + + "for while do if else struct " + + "in out inout"), + blockKeywords: words("for while do if else struct"), + builtin: words("radians degrees sin cos tan asin acos atan " + + "pow exp log exp2 sqrt inversesqrt " + + "abs sign floor ceil fract mod min max clamp mix step smoothstep " + + "length distance dot cross normalize faceforward " + + "reflect refract matrixCompMult " + + "lessThan lessThanEqual greaterThan greaterThanEqual " + + "equal notEqual any all not " + + "texture2D texture2DLod texture2DProjLod " + + "textureCube textureCubeLod "), + atoms: words("true false " + + "gl_FragColor " + + "gl_PointCoord " + + "gl_Position gl_PointSize " + + "gl_FragCoord gl_FrontFacing " + + "gl_FragData " + + "gl_DepthRangeParameters " + + "gl_MaxVertexAttribs gl_MaxVaryingVectors gl_MaxVertexUniformVectors" + + "gl_MaxVertexTextureImageUnits gl_MaxTextureImageUnits " + + "gl_MaxFragmentUniformVectors " + + "gl_MaxDrawBuffers"), + hooks: {"#": cppHook}, + modeProps: {fold: ["brace", "include"]} + }); + +}); diff --git a/package-lock.json b/package-lock.json index 42c2fcd2..23b7a646 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10970,9 +10970,9 @@ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "codemirror": { - "version": "5.34.0", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.34.0.tgz", - "integrity": "sha512-7ke9DJB350sChxq1skTmotVZsJtiJo1ihC41rq8IyOMZv47Z1AQygoevWHs0PJTw2eBphmB7gA3AbPrVrnfwPw==" + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.38.0.tgz", + "integrity": "sha512-PEPnDg8U3DTGFB/Dn2T/INiRNC9CB5k2vLAQJidYCsHvAgtXbklqnuidEwx7yGrMrdGhl0L0P3iNKW9I07J6tQ==" }, "collection-visit": { "version": "1.0.0", diff --git a/package.json b/package.json index 3eff8bf5..bc53a3a2 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "bson-objectid": "^1.1.4", "classnames": "^2.2.5", "clipboard": "^1.7.1", - "codemirror": "^5.21.0", + "codemirror": "^5.38.0", "connect-mongo": "^1.2.0", "cookie-parser": "^1.4.1", "cors": "^2.8.1", diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index 3f05e293..3b14e1eb 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -3,7 +3,7 @@ export const fileExtensionsArray = ['gif', 'jpg', 'jpeg', 'png', 'bmp', 'wav', 'flac', 'ogg', 'oga', 'mp4', 'm4p', 'mp3', 'm4a', 'aiff', 'aif', 'm4v', 'aac', 'webm', 'mpg', 'mp2', 'mpeg', 'mpe', 'mpv', 'js', 'jsx', 'html', 'htm', 'css', 'json', 'csv', 'obj', 'svg', - 'otf', 'ttf', 'txt', 'mov']; + 'otf', 'ttf', 'txt', 'mov', 'vert', 'frag']; export const mimeTypes = `image/*,audio/*,text/javascript,text/html,text/css, application/json,application/x-font-ttf,application/x-font-truetype,text/plain, @@ -19,6 +19,9 @@ export const MEDIA_FILE_QUOTED_REGEX = new RegExp(`^('|")(?!(http:\\/\\/|https:\\/\\/)).*\\.(${fileExtensionsArray.join('|')})('|")$`, 'i'); export const STRING_REGEX = /(['"])((\\\1|.)*?)\1/gm; -export const TEXT_FILE_REGEX = /(.+\.json$|.+\.txt$|.+\.csv$)/i; +// these are files that have to be linked to with a blob url +export const PLAINTEXT_FILE_REGEX = /.+\.(json|txt|csv|vert|frag)$/i; +// these are files that users would want to edit as text (maybe svg should be here?) +export const TEXT_FILE_REGEX = /.+\.(json|txt|csv|vert|frag|js|css|html|htm|jsx)$/i; export const NOT_EXTERNAL_LINK_REGEX = /^(?!(http:\/\/|https:\/\/))/; export const EXTERNAL_LINK_REGEX = /^(http:\/\/|https:\/\/)/; diff --git a/server/utils/previewGeneration.js b/server/utils/previewGeneration.js index 436b667f..61d1e5f5 100644 --- a/server/utils/previewGeneration.js +++ b/server/utils/previewGeneration.js @@ -3,7 +3,7 @@ import { resolvePathToFile } from '../utils/filePath'; import { MEDIA_FILE_REGEX, STRING_REGEX, - TEXT_FILE_REGEX, + PLAINTEXT_FILE_REGEX, EXTERNAL_LINK_REGEX, NOT_EXTERNAL_LINK_REGEX } from './fileUtils'; @@ -21,7 +21,7 @@ function resolveLinksInString(content, files, projectId) { if (resolvedFile) { if (resolvedFile.url) { newContent = newContent.replace(filePath, resolvedFile.url); - } else if (resolvedFile.name.match(TEXT_FILE_REGEX)) { + } else if (resolvedFile.name.match(PLAINTEXT_FILE_REGEX)) { let resolvedFilePath = filePath; if (resolvedFilePath.startsWith('.')) { resolvedFilePath = resolvedFilePath.substr(1);