diff --git a/src/actions/base.js b/src/actions/base.js index e9ff2d0ce..9dfe49c10 100644 --- a/src/actions/base.js +++ b/src/actions/base.js @@ -102,6 +102,11 @@ export const setShouldLogIntoDrawer = shouldLogIntoDrawer => ({ shouldLogIntoDrawer, }); +export const setShouldNotIndentOnExport = shouldNotIndentOnExport => ({ + type: 'SET_SHOULD_NOT_INDENT_ON_EXPORT', + shouldNotIndentOnExport, +}); + export const setShouldStoreSettingsInSyncBackend = newShouldStoreSettingsInSyncBackend => { return (dispatch, getState) => { dispatch({ diff --git a/src/actions/org.js b/src/actions/org.js index 5d51a5696..d7b7bf5a8 100644 --- a/src/actions/org.js +++ b/src/actions/org.js @@ -112,7 +112,8 @@ const doSync = ({ getState().org.present.get('headers'), getState().org.present.get('todoKeywordSets'), getState().org.present.get('fileConfigLines'), - getState().org.present.get('linesBeforeHeadings') + getState().org.present.get('linesBeforeHeadings'), + getState().base.get('shouldNotIndentOnExport') ) ) .then(() => { diff --git a/src/components/OrgFile/OrgFile.unit.test.js b/src/components/OrgFile/OrgFile.unit.test.js index 53aaeec3f..5eafb25e8 100644 --- a/src/components/OrgFile/OrgFile.unit.test.js +++ b/src/components/OrgFile/OrgFile.unit.test.js @@ -13,14 +13,21 @@ import { fromJS } from 'immutable'; * This is a convenience wrapper around parsing an org file using * `parseOrg` and then export it using `exportOrg`. * @param {String} testOrgFile - contents of an org file + * @param {Boolean} dontIndent - by default false, so indent drawers */ -function parseAndExportOrgFile(testOrgFile) { +function parseAndExportOrgFile(testOrgFile, dontIndent = false) { const parsedFile = parseOrg(testOrgFile); const headers = parsedFile.get('headers'); const todoKeywordSets = parsedFile.get('todoKeywordSets'); const fileConfigLines = parsedFile.get('fileConfigLines'); const linesBeforeHeadings = parsedFile.get('linesBeforeHeadings'); - const exportedFile = exportOrg(headers, todoKeywordSets, fileConfigLines, linesBeforeHeadings); + const exportedFile = exportOrg( + headers, + todoKeywordSets, + fileConfigLines, + linesBeforeHeadings, + dontIndent + ); return exportedFile; } @@ -40,25 +47,25 @@ describe('Tests for export', () => { test('Simple description export of empty description works', () => { const description = ''; const header = createSimpleHeaderWithDescription(description); - expect(createRawDescriptionText(header, false)).toEqual(description); + expect(createRawDescriptionText(header, false, false)).toEqual(description); }); test('Simple description export of empty line works', () => { const description = '\n'; const header = createSimpleHeaderWithDescription(description); - expect(createRawDescriptionText(header, false)).toEqual(description); + expect(createRawDescriptionText(header, false, false)).toEqual(description); }); test('Simple description export of non-empty line works', () => { const description = 'abc\n'; const header = createSimpleHeaderWithDescription(description); - expect(createRawDescriptionText(header, false)).toEqual(description); + expect(createRawDescriptionText(header, false, false)).toEqual(description); }); test('Simple description export of non-empty line without trailing newline works (newline will be added)', () => { const description = 'abc'; const header = createSimpleHeaderWithDescription(description); - expect(createRawDescriptionText(header, false)).toEqual(`${description}\n`); + expect(createRawDescriptionText(header, false, false)).toEqual(`${description}\n`); }); }); @@ -372,6 +379,12 @@ ${description}`; expect(exportedFile).toEqual(testOrgFile); }); + test('Properties are flush-left when dontIndent is true', () => { + const testOrgFile = readFixture('properties'); + const exportedLines = parseAndExportOrgFile(testOrgFile, true).split('\n'); + expect(exportedLines[2]).toEqual(':PROPERTIES:'); + }); + test('Tags are formatted as is default in Emacs', () => { const testOrgFile = readFixture('tags'); const exportedFile = parseAndExportOrgFile(testOrgFile); @@ -385,6 +398,18 @@ ${description}`; const exportedFile = parseAndExportOrgFile(testOrgFile); expect(exportedFile).toEqual(testOrgFile); }); + test('Logbook entries are indented by default', () => { + const testOrgFile = readFixture('logbook'); + const exportedLines = parseAndExportOrgFile(testOrgFile).split('\n'); + expect(exportedLines[1]).toEqual(' :LOGBOOK:'); + expect(exportedLines[2].startsWith(' CLOCK:')).toBeTruthy(); + }); + test('Logbook entries are not indented when dontIndent', () => { + const testOrgFile = readFixture('logbook'); + const exportedLines = parseAndExportOrgFile(testOrgFile, true).split('\n'); + expect(exportedLines[1]).toEqual(':LOGBOOK:'); + expect(exportedLines[2].startsWith('CLOCK:')).toBeTruthy(); + }); }); }); diff --git a/src/components/OrgFile/components/HeaderContent/index.js b/src/components/OrgFile/components/HeaderContent/index.js index 155ea82b7..44fd61990 100644 --- a/src/components/OrgFile/components/HeaderContent/index.js +++ b/src/components/OrgFile/components/HeaderContent/index.js @@ -78,7 +78,8 @@ class HeaderContent extends PureComponent { } calculateRawDescription(header) { - return createRawDescriptionText(header, false); + // this is for display only, se we keep the default indentation behaviour + return createRawDescriptionText(header, false, false); } handleTextareaRef(textarea) { diff --git a/src/components/Settings/index.js b/src/components/Settings/index.js index d9a151e3f..757c96734 100644 --- a/src/components/Settings/index.js +++ b/src/components/Settings/index.js @@ -21,6 +21,7 @@ const Settings = ({ shouldSyncOnBecomingVisibile, shouldShowTitleInOrgFile, shouldLogIntoDrawer, + shouldNotIndentOnExport, agendaDefaultDeadlineDelayValue, agendaDefaultDeadlineDelayUnit, hasUnseenChangelog, @@ -57,6 +58,9 @@ const Settings = ({ const handleShouldLogIntoDrawer = () => base.setShouldLogIntoDrawer(!shouldLogIntoDrawer); + const handleShouldNotIndentOnExport = () => + base.setShouldNotIndentOnExport(!shouldNotIndentOnExport); + const handleShouldStoreSettingsInSyncBackendChange = () => base.setShouldStoreSettingsInSyncBackend(!shouldStoreSettingsInSyncBackend); @@ -137,6 +141,22 @@ const Settings = ({ +
+
+ Disable hard indent on Org export +
+ By default, the metadata body (including deadlines and drawers) of an exported org + heading is indented according to its level. If instead you prefer to keep your body text + flush-left, i.e.{' '} + + (setq org-adapt-indentation nil) + + , then activate this setting. The raw content text is left unchanged. +
+
+ +
+
Store settings in sync backend @@ -233,6 +253,7 @@ const mapStateToProps = (state, props) => { shouldSyncOnBecomingVisibile: state.base.get('shouldSyncOnBecomingVisibile'), shouldShowTitleInOrgFile: state.base.get('shouldShowTitleInOrgFile'), shouldLogIntoDrawer: state.base.get('shouldLogIntoDrawer'), + shouldNotIndentOnExport: state.base.get('shouldNotIndentOnExport'), hasUnseenChangelog: state.base.get('hasUnseenChangelog'), }; }; diff --git a/src/lib/export_org.js b/src/lib/export_org.js index 894d6b498..940ab625e 100644 --- a/src/lib/export_org.js +++ b/src/lib/export_org.js @@ -241,7 +241,23 @@ export const generateTitleLine = (header, includeStars) => { return contents; }; -export const exportOrg = (headers, todoKeywordSets, fileConfigLines, linesBeforeHeadings) => { +/** + * Convert state data into a fully rendered Orgmode file text. + * + * @param {*} headers Full list of org headings from redux state. + * @param {*} todoKeywordSets + * @param {*} fileConfigLines List of all "#+VAR:" config lines in the file. + * @param {*} linesBeforeHeadings Text that occurs before the first heading. + * @param {boolean} dontIndent Default false means indent drawers according to + * nesting level, else keep everything flush-left. Description is kept as is. + */ +export const exportOrg = ( + headers, + todoKeywordSets, + fileConfigLines, + linesBeforeHeadings, + dontIndent +) => { let configContent = ''; if (fileConfigLines.size > 0) { @@ -267,18 +283,25 @@ export const exportOrg = (headers, todoKeywordSets, fileConfigLines, linesBefore configContent = configContent + '\n'; } - const headerContent = headers.map(x => createRawDescriptionText(x, true)).join(''); + const headerContent = headers.map(x => createRawDescriptionText(x, true, dontIndent)).join(''); return configContent + headerContent; }; -export const createRawDescriptionText = (header, includeTitle) => { +/** + * Transform state data of complete org element / header into rendered text. + * + * @param {*} header Immutable map with header data + * @param {boolean} includeTitle Render the full orgmode title + * @param {boolean} dontIndent If true keep all text flush-left, else (DEFAULT) indent according to nesting level + */ +export const createRawDescriptionText = (header, includeTitle, dontIndent) => { // To simplify access to properties: header = header.toJS(); // Pad things like planning items and tables appropriately - // considering the nestingLevel of the header. - const indentation = ' '.repeat(header.nestingLevel + 1); + // considering the nestingLevel of the header, unless that is explicitly disabled + const indentation = dontIndent ? '' : ' '.repeat(header.nestingLevel + 1); let contents = ''; if (includeTitle) { diff --git a/src/lib/parse_org.unit.test.js b/src/lib/parse_org.unit.test.js index 9510e1e60..dad33289a 100644 --- a/src/lib/parse_org.unit.test.js +++ b/src/lib/parse_org.unit.test.js @@ -20,7 +20,8 @@ function parseAndExportOrgFile(testOrgFile) { const todoKeywordSets = parsedFile.get('todoKeywordSets'); const fileConfigLines = parsedFile.get('fileConfigLines'); const linesBeforeHeadings = parsedFile.get('linesBeforeHeadings'); - const exportedFile = exportOrg(headers, todoKeywordSets, fileConfigLines, linesBeforeHeadings); + const exportedFile = exportOrg( + headers, todoKeywordSets, fileConfigLines, linesBeforeHeadings, false); return exportedFile; } diff --git a/src/reducers/base.js b/src/reducers/base.js index befd8e718..ef75bfff8 100644 --- a/src/reducers/base.js +++ b/src/reducers/base.js @@ -33,6 +33,14 @@ const setShouldShowTitleInOrgFile = (state, action) => const setShouldLogIntoDrawer = (state, action) => state.set('shouldLogIntoDrawer', action.shouldLogIntoDrawer); +/** + * When enabled, keep all heading body text flush-left. When disabled (the + * default) indent the body text of headings according to the nesting level of + * the heading. + */ +const setShouldNotIndentOnExport = (state, action) => + state.set('shouldNotIndentOnExport', action.shouldNotIndentOnExport); + const setHasUnseenChangelog = (state, action) => state.set('hasUnseenChangelog', action.newHasUnseenChangelog); @@ -129,6 +137,8 @@ export default (state = Map(), action) => { return setShouldShowTitleInOrgFile(state, action); case 'SET_SHOULD_LOG_INTO_DRAWER': return setShouldLogIntoDrawer(state, action); + case 'SET_SHOULD_NOT_INDENT_ON_EXPORT': + return setShouldNotIndentOnExport(state, action); case 'SET_HAS_UNSEEN_CHANGELOG': return setHasUnseenChangelog(state, action); case 'SET_LAST_SEEN_CHANGELOG_HEADER': diff --git a/src/util/settings_persister.js b/src/util/settings_persister.js index 8989c3bce..513eefab1 100644 --- a/src/util/settings_persister.js +++ b/src/util/settings_persister.js @@ -117,6 +117,12 @@ export const persistableFields = [ type: 'boolean', shouldStoreInConfig: true, }, + { + category: 'base', + name: 'shouldNotIndentOnExport', + type: 'boolean', + shouldStoreInConfig: true, + }, { category: 'capture', name: 'captureTemplates',