CheckSubsections extends Rule { @Override public List<Advice> evaluate(AnnotatedString s, AnnotatedString original) { Stack<SectionInfo> sections = new Stack<SectionInfo>(); SectionInfo doc_head = new SectionInfo("", new Range(Position.ZERO, Position.ZERO)); List<Advice> out_list = new ArrayList<Advice>(); List<String> lines = s.getLines(); sections.push(doc_head); for (int line_cnt = 0; line_cnt < lines.size(); line_cnt++) { String line = lines.get(line_cnt); Matcher mat = m_headingPattern.matcher(line); if (mat.find()) { Position start_pos = s.getSourcePosition(new Position(line_cnt, mat.start(1))); Position end_pos = s.getSourcePosition(new Position(line_cnt, mat.start(1) + mat.group(1).length())); String heading = mat.group(1).trim(); SectionInfo si = new SectionInfo(heading, new Range(start_pos, end_pos)); SectionInfo si_last = sections.peek(); if (!si_last.m_sectionName.isEmpty() && SectionInfo.distance(si_last.m_sectionName, si.m_sectionName) < -1) { out_list.add(new Advice(CheckLevelSkip.instance, si.m_range, "A heading of level n should not be followed by a heading of level n+2 or more.", original.getResourceName(), original.getLine(si.m_range.getStart().getLine()), original.getOffset(start_pos))); } if (SectionInfo.isMoveDown(si_last, si)) { si_last.m_size++; sections.push(si); } else { if (si_last.m_sectionName.compareTo(heading) == 0) { sections.pop(); SectionInfo si_parent = sections.peek(); si_parent.m_size++; sections.push(si); } else { while (!sections.isEmpty() && si_last.m_sectionName.compareTo(heading) != 0) { si_last = sections.pop(); if (si_last.m_size == 1) { Range r2 = si_last.m_range; out_list.add(new Advice(this, r2, "If a section has sub-sections, it should have more than one such sub-section.", original.getResourceName(), original.getLine(si_last.m_range.getStart().getLine()), original.getOffset(start_pos))); } } if (sections.isEmpty()) { Range r2 = new Range(original.getSourcePosition(si.m_range.getStart()), original.getSourcePosition(si.m_range.getEnd())); out_list.add(new Advice(CheckSubsectionOrder.instance, r2, "The first heading of a document should be the one with the highest level. For example, if a document contains sections, the first section cannot be preceded by a sub-section.", original.getResourceName(), original.getLine(r2.getStart().getLine()), original.getOffset(start_pos))); sections.push(doc_head); } else { SectionInfo si_parent = sections.peek(); si_parent.m_size++; sections.push(si); } } } } } while (!sections.isEmpty()) { SectionInfo si_last = sections.pop(); if (!si_last.m_sectionName.isEmpty() && si_last.m_size == 1) { Range r2 = si_last.m_range; out_list.add(new Advice(this, r2, "If a section has sub-sections, it should have more than one such sub-section.", original.getResourceName(), original.getLine(si_last.m_range.getStart().getLine()), original.getOffset(r2.getStart()))); } } return out_list; } CheckSubsections(); @Override List<Advice> evaluate(AnnotatedString s, AnnotatedString original); @Override String getDescription();  }
@Test public void test1() { AnnotatedString in_string = AnnotatedString.read(new Scanner(CheckSubsectionsTest.class.getResourceAsStream("data/test-subsec-1.tex"))); Rule r = new CheckSubsections(); List<Advice> ad_list = r.evaluate(in_string, in_string); assertEquals(1, ad_list.size()); Advice ad = ad_list.get(0); assertEquals(2, ad.getRange().getStart().getLine()); }
@Test public void test2() { AnnotatedString in_string = AnnotatedString.read(new Scanner(CheckSubsectionsTest.class.getResourceAsStream("data/test-subsec-2.tex"))); Rule r = new CheckSubsections(); List<Advice> ad_list = r.evaluate(in_string, in_string); assertTrue(ad_list.isEmpty()); }
@Test public void test3() { AnnotatedString in_string = AnnotatedString.read(new Scanner(CheckSubsectionsTest.class.getResourceAsStream("data/test-subsec-3.tex"))); Rule r = new CheckSubsections(); List<Advice> ad_list = r.evaluate(in_string, in_string); assertTrue(containsAdviceWithLabel(ad_list, "sh:secorder")); }